From 7c42373d6b9f70b4616962642c50bdb7f6b408e7 Mon Sep 17 00:00:00 2001 From: Nicholas Cunningham Date: Wed, 15 Jan 2025 11:30:52 -0700 Subject: [PATCH] feat(core): add support for specifying ESLint config file format (mjs/cjs) --- .../generators/convert-to-flat-config.json | 6 + .../migrate-from-angular-cli.spec.ts.snap | 3 +- .../__snapshots__/generator.spec.ts.snap | 781 +++++++- .../converters/json-converter.spec.ts | 869 ++++++--- .../converters/json-converter.ts | 19 +- .../convert-to-flat-config/generator.spec.ts | 1662 ++++++++++++----- .../convert-to-flat-config/generator.ts | 48 +- .../convert-to-flat-config/schema.d.ts | 1 + .../convert-to-flat-config/schema.json | 6 + .../convert-to-inferred.ts | 1 + .../generators/init/global-eslint-config.ts | 78 +- .../src/generators/init/init-migration.ts | 23 +- .../eslint/src/generators/init/init.spec.ts | 124 +- packages/eslint/src/generators/init/init.ts | 23 +- .../lint-project/lint-project.spec.ts | 71 +- .../generators/lint-project/lint-project.ts | 27 +- .../lint-project/setup-root-eslint.ts | 10 +- .../src/generators/utils/eslint-file.ts | 18 +- .../utils/flat-config/ast-utils.spec.ts | 507 ++--- .../generators/utils/flat-config/ast-utils.ts | 752 ++++++-- .../application/application.spec.ts | 35 +- .../__snapshots__/application.spec.ts.snap | 70 +- .../application/application.spec.ts | 17 +- .../__snapshots__/library.spec.ts.snap | 26 +- .../src/generators/library/library.spec.ts | 72 +- 25 files changed, 3969 insertions(+), 1280 deletions(-) diff --git a/docs/generated/packages/eslint/generators/convert-to-flat-config.json b/docs/generated/packages/eslint/generators/convert-to-flat-config.json index 03ad486e88255..89c8d9d641ec2 100644 --- a/docs/generated/packages/eslint/generators/convert-to-flat-config.json +++ b/docs/generated/packages/eslint/generators/convert-to-flat-config.json @@ -13,6 +13,12 @@ "description": "Skip formatting files.", "default": false, "x-priority": "internal" + }, + "eslintConfigFormat": { + "type": "string", + "description": "The format of the ESLint configuration file", + "enum": ["cjs", "mjs"], + "default": "mjs" } }, "additionalProperties": false, diff --git a/packages/angular/src/generators/ng-add/__snapshots__/migrate-from-angular-cli.spec.ts.snap b/packages/angular/src/generators/ng-add/__snapshots__/migrate-from-angular-cli.spec.ts.snap index 92ad21afdcba4..61945c02d8c3f 100644 --- a/packages/angular/src/generators/ng-add/__snapshots__/migrate-from-angular-cli.spec.ts.snap +++ b/packages/angular/src/generators/ng-add/__snapshots__/migrate-from-angular-cli.spec.ts.snap @@ -94,6 +94,7 @@ exports[`workspace move to nx layout should create nx.json 1`] = ` "!{projectRoot}/karma.conf.js", "!{projectRoot}/.eslintrc.json", "!{projectRoot}/eslint.config.cjs", + "!{projectRoot}/eslint.config.mjs", ], "sharedGlobals": [], }, @@ -104,7 +105,7 @@ exports[`workspace move to nx layout should create nx.json 1`] = ` "default", "{workspaceRoot}/.eslintrc.json", "{workspaceRoot}/.eslintignore", - "{workspaceRoot}/eslint.config.cjs", + "{workspaceRoot}/eslint.config.mjs", ], }, "build": { diff --git a/packages/eslint/src/generators/convert-to-flat-config/__snapshots__/generator.spec.ts.snap b/packages/eslint/src/generators/convert-to-flat-config/__snapshots__/generator.spec.ts.snap index 9ecee467df5c7..be513de7fc723 100644 --- a/packages/eslint/src/generators/convert-to-flat-config/__snapshots__/generator.spec.ts.snap +++ b/packages/eslint/src/generators/convert-to-flat-config/__snapshots__/generator.spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`convert-to-flat-config generator should add env configuration 1`] = ` +exports[`convert-to-flat-config generator CJS should add env configuration 1`] = ` "const { FlatCompat } = require('@eslint/eslintrc'); const js = require('@eslint/js'); const nxEslintPlugin = require('@nx/eslint-plugin'); @@ -61,7 +61,7 @@ module.exports = [ " `; -exports[`convert-to-flat-config generator should add global and env configuration 1`] = ` +exports[`convert-to-flat-config generator CJS should add global and env configuration 1`] = ` "const { FlatCompat } = require('@eslint/eslintrc'); const js = require('@eslint/js'); const nxEslintPlugin = require('@nx/eslint-plugin'); @@ -126,7 +126,7 @@ module.exports = [ " `; -exports[`convert-to-flat-config generator should add global configuration 1`] = ` +exports[`convert-to-flat-config generator CJS should add global configuration 1`] = ` "const { FlatCompat } = require('@eslint/eslintrc'); const js = require('@eslint/js'); const nxEslintPlugin = require('@nx/eslint-plugin'); @@ -186,7 +186,7 @@ module.exports = [ " `; -exports[`convert-to-flat-config generator should add global eslintignores 1`] = ` +exports[`convert-to-flat-config generator CJS should add global eslintignores 1`] = ` "const { FlatCompat } = require('@eslint/eslintrc'); const js = require('@eslint/js'); const nxEslintPlugin = require('@nx/eslint-plugin'); @@ -248,7 +248,7 @@ module.exports = [ " `; -exports[`convert-to-flat-config generator should add parser 1`] = ` +exports[`convert-to-flat-config generator CJS should add parser 1`] = ` "const { FlatCompat } = require('@eslint/eslintrc'); const js = require('@eslint/js'); const nxEslintPlugin = require('@nx/eslint-plugin'); @@ -309,7 +309,7 @@ module.exports = [ " `; -exports[`convert-to-flat-config generator should add plugins 1`] = ` +exports[`convert-to-flat-config generator CJS should add plugins 1`] = ` "const { FlatCompat } = require('@eslint/eslintrc'); const js = require('@eslint/js'); const eslintPluginImport = require('eslint-plugin-import'); @@ -378,7 +378,7 @@ module.exports = [ " `; -exports[`convert-to-flat-config generator should add settings 1`] = ` +exports[`convert-to-flat-config generator CJS should add settings 1`] = ` "const { FlatCompat } = require('@eslint/eslintrc'); const js = require('@eslint/js'); const nxEslintPlugin = require('@nx/eslint-plugin'); @@ -442,7 +442,7 @@ module.exports = [ " `; -exports[`convert-to-flat-config generator should convert json successfully 1`] = ` +exports[`convert-to-flat-config generator CJS should convert json successfully 1`] = ` "const { FlatCompat } = require('@eslint/eslintrc'); const js = require('@eslint/js'); const nxEslintPlugin = require('@nx/eslint-plugin'); @@ -501,7 +501,7 @@ module.exports = [ " `; -exports[`convert-to-flat-config generator should convert json successfully 2`] = ` +exports[`convert-to-flat-config generator CJS should convert json successfully 2`] = ` "const baseConfig = require('../../eslint.config.cjs'); module.exports = [ @@ -528,7 +528,7 @@ module.exports = [ " `; -exports[`convert-to-flat-config generator should convert yaml successfully 1`] = ` +exports[`convert-to-flat-config generator CJS should convert yaml successfully 1`] = ` "const { FlatCompat } = require('@eslint/eslintrc'); const js = require('@eslint/js'); const nxEslintPlugin = require('@nx/eslint-plugin'); @@ -587,7 +587,7 @@ module.exports = [ " `; -exports[`convert-to-flat-config generator should convert yaml successfully 2`] = ` +exports[`convert-to-flat-config generator CJS should convert yaml successfully 2`] = ` "const baseConfig = require('../../eslint.config.cjs'); module.exports = [ @@ -614,7 +614,7 @@ module.exports = [ " `; -exports[`convert-to-flat-config generator should convert yml successfully 1`] = ` +exports[`convert-to-flat-config generator CJS should convert yml successfully 1`] = ` "const { FlatCompat } = require('@eslint/eslintrc'); const js = require('@eslint/js'); const nxEslintPlugin = require('@nx/eslint-plugin'); @@ -673,7 +673,7 @@ module.exports = [ " `; -exports[`convert-to-flat-config generator should convert yml successfully 2`] = ` +exports[`convert-to-flat-config generator CJS should convert yml successfully 2`] = ` "const baseConfig = require('../../eslint.config.cjs'); module.exports = [ @@ -700,7 +700,7 @@ module.exports = [ " `; -exports[`convert-to-flat-config generator should handle custom eslintignores 1`] = ` +exports[`convert-to-flat-config generator CJS should handle custom eslintignores 1`] = ` "const baseConfig = require('../../eslint.config.cjs'); module.exports = [ @@ -732,3 +732,756 @@ module.exports = [ ]; " `; + +exports[`convert-to-flat-config generator MJS should add env configuration 1`] = ` +"import { FlatCompat } from '@eslint/eslintrc'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import js from '@eslint/js'; +import nxEslintPlugin from '@nx/eslint-plugin'; +import globals from 'globals'; + +const compat = new FlatCompat({ + baseDirectory: dirname(fileURLToPath(import.meta.url)), + recommendedConfig: js.configs.recommended, +}); + +export default [ + { + ignores: ['**/dist'], + }, + { plugins: { '@nx': nxEslintPlugin } }, + { languageOptions: { globals: { ...globals.browser, ...globals.node } } }, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + rules: { + '@nx/enforce-module-boundaries': [ + 'error', + { + enforceBuildableLibDependency: true, + allow: [], + depConstraints: [ + { + sourceTag: '*', + onlyDependOnLibsWithTags: ['*'], + }, + ], + }, + ], + }, + }, + ...compat + .config({ + extends: ['plugin:@nx/typescript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'], + rules: { + ...config.rules, + }, + })), + ...compat + .config({ + extends: ['plugin:@nx/javascript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'], + rules: { + ...config.rules, + }, + })), +]; +" +`; + +exports[`convert-to-flat-config generator MJS should add global and env configuration 1`] = ` +"import { FlatCompat } from '@eslint/eslintrc'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import js from '@eslint/js'; +import nxEslintPlugin from '@nx/eslint-plugin'; +import globals from 'globals'; + +const compat = new FlatCompat({ + baseDirectory: dirname(fileURLToPath(import.meta.url)), + recommendedConfig: js.configs.recommended, +}); + +export default [ + { + ignores: ['**/dist'], + }, + { plugins: { '@nx': nxEslintPlugin } }, + { + languageOptions: { + globals: { ...globals.browser, myCustomGlobal: 'readonly' }, + }, + }, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + rules: { + '@nx/enforce-module-boundaries': [ + 'error', + { + enforceBuildableLibDependency: true, + allow: [], + depConstraints: [ + { + sourceTag: '*', + onlyDependOnLibsWithTags: ['*'], + }, + ], + }, + ], + }, + }, + ...compat + .config({ + extends: ['plugin:@nx/typescript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'], + rules: { + ...config.rules, + }, + })), + ...compat + .config({ + extends: ['plugin:@nx/javascript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'], + rules: { + ...config.rules, + }, + })), +]; +" +`; + +exports[`convert-to-flat-config generator MJS should add global configuration 1`] = ` +"import { FlatCompat } from '@eslint/eslintrc'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import js from '@eslint/js'; +import nxEslintPlugin from '@nx/eslint-plugin'; + +const compat = new FlatCompat({ + baseDirectory: dirname(fileURLToPath(import.meta.url)), + recommendedConfig: js.configs.recommended, +}); + +export default [ + { + ignores: ['**/dist'], + }, + { plugins: { '@nx': nxEslintPlugin } }, + { languageOptions: { globals: { myCustomGlobal: 'readonly' } } }, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + rules: { + '@nx/enforce-module-boundaries': [ + 'error', + { + enforceBuildableLibDependency: true, + allow: [], + depConstraints: [ + { + sourceTag: '*', + onlyDependOnLibsWithTags: ['*'], + }, + ], + }, + ], + }, + }, + ...compat + .config({ + extends: ['plugin:@nx/typescript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'], + rules: { + ...config.rules, + }, + })), + ...compat + .config({ + extends: ['plugin:@nx/javascript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'], + rules: { + ...config.rules, + }, + })), +]; +" +`; + +exports[`convert-to-flat-config generator MJS should add global eslintignores 1`] = ` +"import { FlatCompat } from '@eslint/eslintrc'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import js from '@eslint/js'; +import nxEslintPlugin from '@nx/eslint-plugin'; + +const compat = new FlatCompat({ + baseDirectory: dirname(fileURLToPath(import.meta.url)), + recommendedConfig: js.configs.recommended, +}); + +export default [ + { + ignores: ['**/dist'], + }, + { plugins: { '@nx': nxEslintPlugin } }, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + rules: { + '@nx/enforce-module-boundaries': [ + 'error', + { + enforceBuildableLibDependency: true, + allow: [], + depConstraints: [ + { + sourceTag: '*', + onlyDependOnLibsWithTags: ['*'], + }, + ], + }, + ], + }, + }, + ...compat + .config({ + extends: ['plugin:@nx/typescript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'], + rules: { + ...config.rules, + }, + })), + ...compat + .config({ + extends: ['plugin:@nx/javascript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'], + rules: { + ...config.rules, + }, + })), + { + ignores: ['ignore/me'], + }, +]; +" +`; + +exports[`convert-to-flat-config generator MJS should add parser 1`] = ` +"import { FlatCompat } from '@eslint/eslintrc'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import js from '@eslint/js'; +import nxEslintPlugin from '@nx/eslint-plugin'; +import typescriptEslintParser from '@typescript-eslint/parser'; + +const compat = new FlatCompat({ + baseDirectory: dirname(fileURLToPath(import.meta.url)), + recommendedConfig: js.configs.recommended, +}); + +export default [ + { + ignores: ['**/dist'], + }, + { plugins: { '@nx': nxEslintPlugin } }, + { languageOptions: { parser: typescriptEslintParser } }, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + rules: { + '@nx/enforce-module-boundaries': [ + 'error', + { + enforceBuildableLibDependency: true, + allow: [], + depConstraints: [ + { + sourceTag: '*', + onlyDependOnLibsWithTags: ['*'], + }, + ], + }, + ], + }, + }, + ...compat + .config({ + extends: ['plugin:@nx/typescript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'], + rules: { + ...config.rules, + }, + })), + ...compat + .config({ + extends: ['plugin:@nx/javascript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'], + rules: { + ...config.rules, + }, + })), +]; +" +`; + +exports[`convert-to-flat-config generator MJS should add plugins 1`] = ` +"import { FlatCompat } from '@eslint/eslintrc'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import js from '@eslint/js'; +import eslintPluginImport from 'eslint-plugin-import'; +import eslintPluginSingleName from 'eslint-plugin-single-name'; +import scopeEslintPluginWithName from '@scope/eslint-plugin-with-name'; +import justScopeEslintPlugin from '@just-scope/eslint-plugin'; + +const compat = new FlatCompat({ + baseDirectory: dirname(fileURLToPath(import.meta.url)), + recommendedConfig: js.configs.recommended, +}); + +export default [ + { + ignores: ['**/dist'], + }, + { + plugins: { + 'eslint-plugin-import': eslintPluginImport, + 'single-name': eslintPluginSingleName, + '@scope/with-name': scopeEslintPluginWithName, + '@just-scope': justScopeEslintPlugin, + }, + }, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + rules: { + '@nx/enforce-module-boundaries': [ + 'error', + { + enforceBuildableLibDependency: true, + allow: [], + depConstraints: [ + { + sourceTag: '*', + onlyDependOnLibsWithTags: ['*'], + }, + ], + }, + ], + }, + }, + ...compat + .config({ + extends: ['plugin:@nx/typescript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'], + rules: { + ...config.rules, + }, + })), + ...compat + .config({ + extends: ['plugin:@nx/javascript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'], + rules: { + ...config.rules, + }, + })), +]; +" +`; + +exports[`convert-to-flat-config generator MJS should add settings 1`] = ` +"import { FlatCompat } from '@eslint/eslintrc'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import js from '@eslint/js'; +import nxEslintPlugin from '@nx/eslint-plugin'; + +const compat = new FlatCompat({ + baseDirectory: dirname(fileURLToPath(import.meta.url)), + recommendedConfig: js.configs.recommended, +}); + +export default [ + { + ignores: ['**/dist'], + }, + { plugins: { '@nx': nxEslintPlugin } }, + { + settings: { + sharedData: 'Hello', + }, + }, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + rules: { + '@nx/enforce-module-boundaries': [ + 'error', + { + enforceBuildableLibDependency: true, + allow: [], + depConstraints: [ + { + sourceTag: '*', + onlyDependOnLibsWithTags: ['*'], + }, + ], + }, + ], + }, + }, + ...compat + .config({ + extends: ['plugin:@nx/typescript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'], + rules: { + ...config.rules, + }, + })), + ...compat + .config({ + extends: ['plugin:@nx/javascript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'], + rules: { + ...config.rules, + }, + })), +]; +" +`; + +exports[`convert-to-flat-config generator MJS should convert json successfully 1`] = ` +"import { FlatCompat } from '@eslint/eslintrc'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import js from '@eslint/js'; +import nxEslintPlugin from '@nx/eslint-plugin'; + +const compat = new FlatCompat({ + baseDirectory: dirname(fileURLToPath(import.meta.url)), + recommendedConfig: js.configs.recommended, +}); + +export default [ + { + ignores: ['**/dist'], + }, + { plugins: { '@nx': nxEslintPlugin } }, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + rules: { + '@nx/enforce-module-boundaries': [ + 'error', + { + enforceBuildableLibDependency: true, + allow: [], + depConstraints: [ + { + sourceTag: '*', + onlyDependOnLibsWithTags: ['*'], + }, + ], + }, + ], + }, + }, + ...compat + .config({ + extends: ['plugin:@nx/typescript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'], + rules: { + ...config.rules, + }, + })), + ...compat + .config({ + extends: ['plugin:@nx/javascript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'], + rules: { + ...config.rules, + }, + })), +]; +" +`; + +exports[`convert-to-flat-config generator MJS should convert json successfully 2`] = ` +"import baseConfig from '../../eslint.config.mjs'; + +export default [ + { + ignores: ['**/dist'], + }, + ...baseConfig, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + // Override or add rules here + rules: {}, + }, + { + files: ['**/*.ts', '**/*.tsx'], + // Override or add rules here + rules: {}, + }, + { + files: ['**/*.js', '**/*.jsx'], + // Override or add rules here + rules: {}, + }, +]; +" +`; + +exports[`convert-to-flat-config generator MJS should convert yaml successfully 1`] = ` +"import { FlatCompat } from '@eslint/eslintrc'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import js from '@eslint/js'; +import nxEslintPlugin from '@nx/eslint-plugin'; + +const compat = new FlatCompat({ + baseDirectory: dirname(fileURLToPath(import.meta.url)), + recommendedConfig: js.configs.recommended, +}); + +export default [ + { + ignores: ['**/dist'], + }, + { plugins: { '@nx': nxEslintPlugin } }, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + rules: { + '@nx/enforce-module-boundaries': [ + 'error', + { + enforceBuildableLibDependency: true, + allow: [], + depConstraints: [ + { + sourceTag: '*', + onlyDependOnLibsWithTags: ['*'], + }, + ], + }, + ], + }, + }, + ...compat + .config({ + extends: ['plugin:@nx/typescript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'], + rules: { + ...config.rules, + }, + })), + ...compat + .config({ + extends: ['plugin:@nx/javascript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'], + rules: { + ...config.rules, + }, + })), +]; +" +`; + +exports[`convert-to-flat-config generator MJS should convert yaml successfully 2`] = ` +"import baseConfig from '../../eslint.config.mjs'; + +export default [ + { + ignores: ['**/dist'], + }, + ...baseConfig, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + // Override or add rules here + rules: {}, + }, + { + files: ['**/*.ts', '**/*.tsx'], + // Override or add rules here + rules: {}, + }, + { + files: ['**/*.js', '**/*.jsx'], + // Override or add rules here + rules: {}, + }, +]; +" +`; + +exports[`convert-to-flat-config generator MJS should convert yml successfully 1`] = ` +"import { FlatCompat } from '@eslint/eslintrc'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import js from '@eslint/js'; +import nxEslintPlugin from '@nx/eslint-plugin'; + +const compat = new FlatCompat({ + baseDirectory: dirname(fileURLToPath(import.meta.url)), + recommendedConfig: js.configs.recommended, +}); + +export default [ + { + ignores: ['**/dist'], + }, + { plugins: { '@nx': nxEslintPlugin } }, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + rules: { + '@nx/enforce-module-boundaries': [ + 'error', + { + enforceBuildableLibDependency: true, + allow: [], + depConstraints: [ + { + sourceTag: '*', + onlyDependOnLibsWithTags: ['*'], + }, + ], + }, + ], + }, + }, + ...compat + .config({ + extends: ['plugin:@nx/typescript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'], + rules: { + ...config.rules, + }, + })), + ...compat + .config({ + extends: ['plugin:@nx/javascript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'], + rules: { + ...config.rules, + }, + })), +]; +" +`; + +exports[`convert-to-flat-config generator MJS should convert yml successfully 2`] = ` +"import baseConfig from '../../eslint.config.mjs'; + +export default [ + { + ignores: ['**/dist'], + }, + ...baseConfig, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + // Override or add rules here + rules: {}, + }, + { + files: ['**/*.ts', '**/*.tsx'], + // Override or add rules here + rules: {}, + }, + { + files: ['**/*.js', '**/*.jsx'], + // Override or add rules here + rules: {}, + }, +]; +" +`; + +exports[`convert-to-flat-config generator MJS should handle custom eslintignores 1`] = ` +"import baseConfig from '../../eslint.config.mjs'; + +export default [ + { + ignores: ['**/dist'], + }, + ...baseConfig, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + // Override or add rules here + rules: {}, + }, + { + files: ['**/*.ts', '**/*.tsx'], + // Override or add rules here + rules: {}, + }, + { + files: ['**/*.js', '**/*.jsx'], + // Override or add rules here + rules: {}, + }, + { + ignores: ['ignore/me'], + }, + { + ignores: ['ignore/me/as/well'], + }, +]; +" +`; diff --git a/packages/eslint/src/generators/convert-to-flat-config/converters/json-converter.spec.ts b/packages/eslint/src/generators/convert-to-flat-config/converters/json-converter.spec.ts index 6d89d8e2c39f0..9bb8ef02ef766 100644 --- a/packages/eslint/src/generators/convert-to-flat-config/converters/json-converter.spec.ts +++ b/packages/eslint/src/generators/convert-to-flat-config/converters/json-converter.spec.ts @@ -9,306 +9,623 @@ describe('convertEslintJsonToFlatConfig', () => { tree = createTreeWithEmptyWorkspace(); }); - it('should convert root configs', async () => { - tree.write( - '.eslintrc.json', - JSON.stringify({ - root: true, - ignorePatterns: ['**/*', 'src/ignore/to/keep.ts'], - plugins: ['@nx'], - overrides: [ - { - files: ['*.ts', '*.tsx', '*.js', '*.jsx'], - rules: { - '@nx/enforce-module-boundaries': [ - 'error', - { - enforceBuildableLibDependency: true, - allow: [], - depConstraints: [ - { - sourceTag: '*', - onlyDependOnLibsWithTags: ['*'], - }, - ], - }, + describe('ESM', () => { + it('should convert root configs', async () => { + tree.write( + '.eslintrc.json', + JSON.stringify({ + root: true, + ignorePatterns: ['**/*', 'src/ignore/to/keep.ts'], + plugins: ['@nx'], + overrides: [ + { + files: ['*.ts', '*.tsx', '*.js', '*.jsx'], + rules: { + '@nx/enforce-module-boundaries': [ + 'error', + { + enforceBuildableLibDependency: true, + allow: [], + depConstraints: [ + { + sourceTag: '*', + onlyDependOnLibsWithTags: ['*'], + }, + ], + }, + ], + }, + }, + { + files: ['*.ts', '*.tsx'], + extends: ['plugin:@nx/typescript'], + rules: {}, + }, + { + files: ['*.js', '*.jsx'], + extends: ['plugin:@nx/javascript'], + rules: {}, + }, + + { + files: [ + '**/*.spec.ts', + '**/*.spec.tsx', + '**/*.spec.js', + '**/*.spec.jsx', ], + env: { + jest: true, + }, + rules: {}, }, - }, - { - files: ['*.ts', '*.tsx'], - extends: ['plugin:@nx/typescript'], - rules: {}, - }, - { - files: ['*.js', '*.jsx'], - extends: ['plugin:@nx/javascript'], - rules: {}, - }, + ], + }) + ); + + tree.write('.eslintignore', 'node_modules\nsomething/else'); + + const { content } = convertEslintJsonToFlatConfig( + tree, + '', + readJson(tree, '.eslintrc.json'), + ['.eslintignore'], + 'mjs' + ); + + expect(content).toMatchInlineSnapshot(` + "import { FlatCompat } from "@eslint/eslintrc"; + import { dirname } from "path"; + import { fileURLToPath } from "url"; + import js from "@eslint/js"; + import nxEslintPlugin from "@nx/eslint-plugin"; - { - files: [ - '**/*.spec.ts', - '**/*.spec.tsx', - '**/*.spec.js', - '**/*.spec.jsx', - ], - env: { - jest: true, + const compat = new FlatCompat({ + baseDirectory: dirname(fileURLToPath(import.meta.url)), + recommendedConfig: js.configs.recommended, + }); + + + export default [ + { + ignores: [ + "**/dist" + ] + }, + { plugins: { "@nx": nxEslintPlugin } }, + { + files: [ + "**/*.ts", + "**/*.tsx", + "**/*.js", + "**/*.jsx" + ], + rules: { + "@nx/enforce-module-boundaries": [ + "error", + { + enforceBuildableLibDependency: true, + allow: [], + depConstraints: [ + { + sourceTag: "*", + onlyDependOnLibsWithTags: [ + "*" + ] + } + ] + } + ] + } + }, + ...compat.config({ + extends: [ + "plugin:@nx/typescript" + ] + }).map(config => ({ + ...config, + files: [ + "**/*.ts", + "**/*.tsx", + "**/*.cts", + "**/*.mts" + ], + rules: { + ...config.rules + } + })), + ...compat.config({ + extends: [ + "plugin:@nx/javascript" + ] + }).map(config => ({ + ...config, + files: [ + "**/*.js", + "**/*.jsx", + "**/*.cjs", + "**/*.mjs" + ], + rules: { + ...config.rules + } + })), + ...compat.config({ + env: { + jest: true + } + }).map(config => ({ + ...config, + files: [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.spec.js", + "**/*.spec.jsx" + ], + rules: { + ...config.rules + } + })), + { + ignores: [ + "src/ignore/to/keep.ts" + ] + }, + { + ignores: [ + "something/else" + ] + } + ]; + " + `); + }); + + it('should convert project configs', async () => { + tree.write( + 'mylib/.eslintrc.json', + JSON.stringify({ + extends: [ + 'plugin:@nx/react-typescript', + 'next', + 'next/core-web-vitals', + '../../.eslintrc.json', + ], + ignorePatterns: ['!**/*', '.next/**/*'], + overrides: [ + { + files: ['*.ts', '*.tsx', '*.js', '*.jsx'], + rules: { + '@next/next/no-html-link-for-pages': [ + 'error', + 'apps/test-next/pages', + ], + }, + }, + { + files: ['*.ts', '*.tsx'], + rules: {}, }, - rules: {}, + { + files: ['*.js', '*.jsx'], + rules: {}, + }, + { + files: ['*.json'], + parser: 'jsonc-eslint-parser', + rules: { + '@nx/dependency-checks': 'error', + }, + }, + ], + rules: { + '@next/next/no-html-link-for-pages': 'off', }, - ], - }) - ); + env: { + jest: true, + }, + }) + ); - tree.write('.eslintignore', 'node_modules\nsomething/else'); + tree.write('mylib/.eslintignore', 'node_modules\nsomething/else'); - const { content } = convertEslintJsonToFlatConfig( - tree, - '', - readJson(tree, '.eslintrc.json'), - ['.eslintignore'] - ); + const { content } = convertEslintJsonToFlatConfig( + tree, + 'mylib', + readJson(tree, 'mylib/.eslintrc.json'), + ['mylib/.eslintignore'], + 'mjs' + ); - expect(content).toMatchInlineSnapshot(` - "import { FlatCompat } from "@eslint/eslintrc"; - import js from "@eslint/js"; - import nxEslintPlugin from "@nx/eslint-plugin"; - export const myExports = [ - { - ignores: [ - "**/dist" - ] - }, - { plugins: { "@nx": nxEslintPlugin } }, - { - files: [ - "**/*.ts", - "**/*.tsx", - "**/*.js", - "**/*.jsx" - ], - rules: { - "@nx/enforce-module-boundaries": [ - "error", - { - enforceBuildableLibDependency: true, - allow: [], - depConstraints: [ - { - sourceTag: "*", - onlyDependOnLibsWithTags: [ - "*" - ] - } + expect(content).toMatchInlineSnapshot(` + "import { FlatCompat } from "@eslint/eslintrc"; + import { dirname } from "path"; + import { fileURLToPath } from "url"; + import js from "@eslint/js"; + import baseConfig from "../../eslint.config.mjs"; + import globals from "globals"; + + const compat = new FlatCompat({ + baseDirectory: dirname(fileURLToPath(import.meta.url)), + recommendedConfig: js.configs.recommended, + }); + + + export default [ + { + ignores: [ + "**/dist" + ] + }, + ...baseConfig, + ...compat.extends("plugin:@nx/react-typescript", "next", "next/core-web-vitals"), + { languageOptions: { globals: { ...globals.jest } } }, + { + rules: { + "@next/next/no-html-link-for-pages": "off" + } + }, + { + files: [ + "**/*.ts", + "**/*.tsx", + "**/*.js", + "**/*.jsx" + ], + rules: { + "@next/next/no-html-link-for-pages": [ + "error", + "apps/test-next/pages" ] } - ] - } - }, - ...compat.config({ - extends: [ - "plugin:@nx/typescript" - ] - }).map(config => ({ - ...config, - files: [ - "**/*.ts", - "**/*.tsx", - "**/*.cts", - "**/*.mts" - ], + }, + { + files: [ + "**/*.ts", + "**/*.tsx" + ], + // Override or add rules here + rules: {} + }, + { + files: [ + "**/*.js", + "**/*.jsx" + ], + // Override or add rules here + rules: {} + }, + { + files: [ + "**/*.json" + ], + rules: { + "@nx/dependency-checks": "error" + }, + languageOptions: { + parser: await import("jsonc-eslint-parser") + } + }, + { + ignores: [ + ".next/**/*" + ] + }, + { + ignores: [ + "something/else" + ] + } + ]; + " + `); + }); + }); + + describe('CJS', () => { + it('should convert root configs', async () => { + tree.write( + '.eslintrc.json', + JSON.stringify({ + root: true, + ignorePatterns: ['**/*', 'src/ignore/to/keep.ts'], + plugins: ['@nx'], + overrides: [ + { + files: ['*.ts', '*.tsx', '*.js', '*.jsx'], rules: { - ...config.rules - } - })), - ...compat.config({ - extends: [ - "plugin:@nx/javascript" - ] - }).map(config => ({ - ...config, + '@nx/enforce-module-boundaries': [ + 'error', + { + enforceBuildableLibDependency: true, + allow: [], + depConstraints: [ + { + sourceTag: '*', + onlyDependOnLibsWithTags: ['*'], + }, + ], + }, + ], + }, + }, + { + files: ['*.ts', '*.tsx'], + extends: ['plugin:@nx/typescript'], + rules: {}, + }, + { + files: ['*.js', '*.jsx'], + extends: ['plugin:@nx/javascript'], + rules: {}, + }, + + { files: [ - "**/*.js", - "**/*.jsx", - "**/*.cjs", - "**/*.mjs" + '**/*.spec.ts', + '**/*.spec.tsx', + '**/*.spec.js', + '**/*.spec.jsx', ], - rules: { - ...config.rules - } - })), - ...compat.config({ env: { - jest: true - } - }).map(config => ({ - ...config, - files: [ - "**/*.spec.ts", - "**/*.spec.tsx", - "**/*.spec.js", - "**/*.spec.jsx" - ], - rules: { - ...config.rules - } - })), - { - ignores: [ - "src/ignore/to/keep.ts" - ] - }, - { - ignores: [ - "something/else" - ] - } - ]; - const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, - }); + jest: true, + }, + rules: {}, + }, + ], + }) + ); - export default myExports; - " - `); - }); + tree.write('.eslintignore', 'node_modules\nsomething/else'); - it('should convert project configs', async () => { - tree.write( - 'mylib/.eslintrc.json', - JSON.stringify({ - extends: [ - 'plugin:@nx/react-typescript', - 'next', - 'next/core-web-vitals', - '../../.eslintrc.json', - ], - ignorePatterns: ['!**/*', '.next/**/*'], - overrides: [ - { - files: ['*.ts', '*.tsx', '*.js', '*.jsx'], - rules: { - '@next/next/no-html-link-for-pages': [ - 'error', - 'apps/test-next/pages', - ], - }, - }, - { - files: ['*.ts', '*.tsx'], - rules: {}, - }, - { - files: ['*.js', '*.jsx'], - rules: {}, - }, - { - files: ['*.json'], - parser: 'jsonc-eslint-parser', - rules: { - '@nx/dependency-checks': 'error', - }, - }, - ], - rules: { - '@next/next/no-html-link-for-pages': 'off', - }, - env: { - jest: true, - }, - }) - ); + const { content } = convertEslintJsonToFlatConfig( + tree, + '', + readJson(tree, '.eslintrc.json'), + ['.eslintignore'], + 'cjs' + ); - tree.write('mylib/.eslintignore', 'node_modules\nsomething/else'); + expect(content).toMatchInlineSnapshot(` + "const { FlatCompat } = require("@eslint/eslintrc"); + const js = require("@eslint/js"); + const nxEslintPlugin = require("@nx/eslint-plugin"); - const { content } = convertEslintJsonToFlatConfig( - tree, - 'mylib', - readJson(tree, 'mylib/.eslintrc.json'), - ['mylib/.eslintignore'] - ); + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); - expect(content).toMatchInlineSnapshot(` - "import { FlatCompat } from "@eslint/eslintrc"; - import js from "@eslint/js"; - import baseConfig from "../../eslint.config.mjs"; - import globals from "globals"; - export const myExports = [ - { - ignores: [ - "**/dist" - ] - }, - ...baseConfig, - ...compat.extends("plugin:@nx/react-typescript", "next", "next/core-web-vitals"), - { languageOptions: { globals: { ...globals.jest } } }, - { - rules: { - "@next/next/no-html-link-for-pages": "off" - } - }, - { - files: [ - "**/*.ts", - "**/*.tsx", - "**/*.js", - "**/*.jsx" - ], + module.exports = [ + { + ignores: [ + "**/dist" + ] + }, + { plugins: { "@nx": nxEslintPlugin } }, + { + files: [ + "**/*.ts", + "**/*.tsx", + "**/*.js", + "**/*.jsx" + ], + rules: { + "@nx/enforce-module-boundaries": [ + "error", + { + enforceBuildableLibDependency: true, + allow: [], + depConstraints: [ + { + sourceTag: "*", + onlyDependOnLibsWithTags: [ + "*" + ] + } + ] + } + ] + } + }, + ...compat.config({ + extends: [ + "plugin:@nx/typescript" + ] + }).map(config => ({ + ...config, + files: [ + "**/*.ts", + "**/*.tsx", + "**/*.cts", + "**/*.mts" + ], + rules: { + ...config.rules + } + })), + ...compat.config({ + extends: [ + "plugin:@nx/javascript" + ] + }).map(config => ({ + ...config, + files: [ + "**/*.js", + "**/*.jsx", + "**/*.cjs", + "**/*.mjs" + ], + rules: { + ...config.rules + } + })), + ...compat.config({ + env: { + jest: true + } + }).map(config => ({ + ...config, + files: [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.spec.js", + "**/*.spec.jsx" + ], + rules: { + ...config.rules + } + })), + { + ignores: [ + "src/ignore/to/keep.ts" + ] + }, + { + ignores: [ + "something/else" + ] + } + ]; + " + `); + }); + + it('should convert project configs', async () => { + tree.write( + 'mylib/.eslintrc.json', + JSON.stringify({ + extends: [ + 'plugin:@nx/react-typescript', + 'next', + 'next/core-web-vitals', + '../../.eslintrc.json', + ], + ignorePatterns: ['!**/*', '.next/**/*'], + overrides: [ + { + files: ['*.ts', '*.tsx', '*.js', '*.jsx'], rules: { - "@next/next/no-html-link-for-pages": [ - "error", - "apps/test-next/pages" - ] - } - }, - { - files: [ - "**/*.ts", - "**/*.tsx" - ], - // Override or add rules here - rules: {} - }, - { - files: [ - "**/*.js", - "**/*.jsx" - ], - // Override or add rules here - rules: {} - }, - { - files: [ - "**/*.json" - ], + '@next/next/no-html-link-for-pages': [ + 'error', + 'apps/test-next/pages', + ], + }, + }, + { + files: ['*.ts', '*.tsx'], + rules: {}, + }, + { + files: ['*.js', '*.jsx'], + rules: {}, + }, + { + files: ['*.json'], + parser: 'jsonc-eslint-parser', rules: { - "@nx/dependency-checks": "error" + '@nx/dependency-checks': 'error', }, - languageOptions: { - parser: await import("jsonc-eslint-parser") - } + }, + ], + rules: { + '@next/next/no-html-link-for-pages': 'off', }, - { - ignores: [ - ".next/**/*" - ] + env: { + jest: true, }, - { - ignores: [ - "something/else" - ] - } - ]; - const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, - }); + }) + ); - export default myExports; - " - `); + tree.write('mylib/.eslintignore', 'node_modules\nsomething/else'); + + const { content } = convertEslintJsonToFlatConfig( + tree, + 'mylib', + readJson(tree, 'mylib/.eslintrc.json'), + ['mylib/.eslintignore'], + 'cjs' + ); + + expect(content).toMatchInlineSnapshot(` + "const { FlatCompat } = require("@eslint/eslintrc"); + const js = require("@eslint/js"); + const baseConfig = require("../../eslint.config.cjs"); + const globals = require("globals"); + + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + + module.exports = [ + { + ignores: [ + "**/dist" + ] + }, + ...baseConfig, + ...compat.extends("plugin:@nx/react-typescript", "next", "next/core-web-vitals"), + { languageOptions: { globals: { ...globals.jest } } }, + { + rules: { + "@next/next/no-html-link-for-pages": "off" + } + }, + { + files: [ + "**/*.ts", + "**/*.tsx", + "**/*.js", + "**/*.jsx" + ], + rules: { + "@next/next/no-html-link-for-pages": [ + "error", + "apps/test-next/pages" + ] + } + }, + { + files: [ + "**/*.ts", + "**/*.tsx" + ], + // Override or add rules here + rules: {} + }, + { + files: [ + "**/*.js", + "**/*.jsx" + ], + // Override or add rules here + rules: {} + }, + { + files: [ + "**/*.json" + ], + rules: { + "@nx/dependency-checks": "error" + }, + languageOptions: { + parser: require("jsonc-eslint-parser") + } + }, + { + ignores: [ + ".next/**/*" + ] + }, + { + ignores: [ + "something/else" + ] + } + ]; + " + `); + }); }); }); diff --git a/packages/eslint/src/generators/convert-to-flat-config/converters/json-converter.ts b/packages/eslint/src/generators/convert-to-flat-config/converters/json-converter.ts index 3e17ac1c34e89..18fa09e779729 100644 --- a/packages/eslint/src/generators/convert-to-flat-config/converters/json-converter.ts +++ b/packages/eslint/src/generators/convert-to-flat-config/converters/json-converter.ts @@ -21,7 +21,8 @@ export function convertEslintJsonToFlatConfig( tree: Tree, root: string, config: ESLint.ConfigData, - ignorePaths: string[] + ignorePaths: string[], + format: 'cjs' | 'mjs' ): { content: string; addESLintRC: boolean; addESLintJS: boolean } { const importsMap = new Map(); const exportElements: ts.Expression[] = []; @@ -38,7 +39,12 @@ export function convertEslintJsonToFlatConfig( ); if (config.extends) { - const extendsResult = addExtends(importsMap, exportElements, config); + const extendsResult = addExtends( + importsMap, + exportElements, + config, + format + ); isFlatCompatNeeded = extendsResult.isFlatCompatNeeded; isESLintJSNeeded = extendsResult.isESLintJSNeeded; } @@ -156,7 +162,7 @@ export function convertEslintJsonToFlatConfig( ) { isFlatCompatNeeded = true; } - exportElements.push(generateFlatOverride(override)); + exportElements.push(generateFlatOverride(override, format)); }); } @@ -189,7 +195,7 @@ export function convertEslintJsonToFlatConfig( } // create the node list and print it to new file - const nodeList = createNodeList(importsMap, exportElements); + const nodeList = createNodeList(importsMap, exportElements, format); let content = stringifyNodeList(nodeList); if (isFlatCompatNeeded) { content = addFlatCompatToFlatConfig(content); @@ -206,7 +212,8 @@ export function convertEslintJsonToFlatConfig( function addExtends( importsMap: Map, configBlocks: ts.Expression[], - config: ESLint.ConfigData + config: ESLint.ConfigData, + format: 'mjs' | 'cjs' ): { isFlatCompatNeeded: boolean; isESLintJSNeeded: boolean } { let isFlatCompatNeeded = false; let isESLintJSNeeded = false; @@ -225,7 +232,7 @@ function addExtends( configBlocks.push(generateSpreadElement(localName)); const newImport = imp.replace( /^(.*)\.eslintrc(.base)?\.json$/, - '$1eslint$2.config.mjs' + `$1eslint$2.config.${format}` ); importsMap.set(newImport, localName); } else { diff --git a/packages/eslint/src/generators/convert-to-flat-config/generator.spec.ts b/packages/eslint/src/generators/convert-to-flat-config/generator.spec.ts index 6ea81f9c79bc7..708079b128b7b 100644 --- a/packages/eslint/src/generators/convert-to-flat-config/generator.spec.ts +++ b/packages/eslint/src/generators/convert-to-flat-config/generator.spec.ts @@ -19,7 +19,6 @@ import { dump } from '@zkochan/js-yaml'; describe('convert-to-flat-config generator', () => { let tree: Tree; - const options: ConvertToFlatConfigGeneratorSchema = { skipFormat: false }; // TODO(@meeroslav): add plugin in these tests @@ -47,593 +46,1232 @@ describe('convert-to-flat-config generator', () => { }); }); - it('should update dependencies', async () => { - await lintProjectGenerator(tree, { + describe('CJS', () => { + const options: ConvertToFlatConfigGeneratorSchema = { skipFormat: false, - linter: Linter.EsLint, - project: 'test-lib', - setParserOptionsProject: false, - }); - await convertToFlatConfigGenerator(tree, options); - - expect(tree.read('package.json', 'utf-8')).toMatchInlineSnapshot(` - "{ - "name": "@proj/source", - "dependencies": {}, - "devDependencies": { - "@eslint/eslintrc": "^2.1.1", - "@nx/eslint": "0.0.1", - "@nx/eslint-plugin": "0.0.1", - "eslint": "^9.8.0", - "eslint-config-prettier": "^9.0.0", - "typescript-eslint": "^8.13.0" - } - } - " - `); - }); - - it('should convert json successfully', async () => { - await lintProjectGenerator(tree, { - skipFormat: false, - linter: Linter.EsLint, - project: 'test-lib', - setParserOptionsProject: false, - }); - await convertToFlatConfigGenerator(tree, options); - - expect(tree.exists('eslint.config.cjs')).toBeTruthy(); - expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot(); - expect(tree.exists('libs/test-lib/eslint.config.mjs')).toBeTruthy(); - expect( - tree.read('libs/test-lib/eslint.config.cjs', 'utf-8') - ).toMatchSnapshot(); - // check nx.json changes - const nxJson = readJson(tree, 'nx.json'); - expect(nxJson.targetDefaults.lint.inputs).toContain( - '{workspaceRoot}/eslint.config.mjs' - ); - expect(nxJson.namedInputs.production).toContain( - '!{projectRoot}/eslint.config.mjs' - ); - }); + eslintConfigFormat: 'cjs', + }; + it('should update dependencies', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'cjs', + }); + await convertToFlatConfigGenerator(tree, options); - it('should convert yaml successfully', async () => { - await lintProjectGenerator(tree, { - skipFormat: false, - linter: Linter.EsLint, - eslintFilePatterns: ['**/*.ts'], - project: 'test-lib', - setParserOptionsProject: false, - }); - const yamlContent = dump(readJson(tree, 'libs/test-lib/.eslintrc.json')); - tree.delete('libs/test-lib/.eslintrc.json'); - tree.write('libs/test-lib/.eslintrc.yaml', yamlContent); - - await convertToFlatConfigGenerator(tree, options); - - expect(tree.exists('eslint.config.mjs')).toBeTruthy(); - expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchSnapshot(); - expect(tree.exists('libs/test-lib/eslint.config.mjs')).toBeTruthy(); - expect( - tree.read('libs/test-lib/eslint.config.mjs', 'utf-8') - ).toMatchSnapshot(); - // check nx.json changes - const nxJson = readJson(tree, 'nx.json'); - expect(nxJson.targetDefaults.lint.inputs).toContain( - '{workspaceRoot}/eslint.config.cjs' - ); - expect(nxJson.namedInputs.production).toContain( - '!{projectRoot}/eslint.config.mjs' - ); - }); + expect(tree.read('package.json', 'utf-8')).toMatchInlineSnapshot(` + "{ + "name": "@proj/source", + "dependencies": {}, + "devDependencies": { + "@eslint/eslintrc": "^2.1.1", + "@nx/eslint": "0.0.1", + "@nx/eslint-plugin": "0.0.1", + "eslint": "^9.8.0", + "eslint-config-prettier": "^9.0.0", + "typescript-eslint": "^8.13.0" + } + } + " + `); + }); - it('should convert yml successfully', async () => { - await lintProjectGenerator(tree, { - skipFormat: false, - linter: Linter.EsLint, - eslintFilePatterns: ['**/*.ts'], - project: 'test-lib', - setParserOptionsProject: false, - }); - const yamlContent = dump(readJson(tree, 'libs/test-lib/.eslintrc.json')); - tree.delete('libs/test-lib/.eslintrc.json'); - tree.write('libs/test-lib/.eslintrc.yml', yamlContent); - - await convertToFlatConfigGenerator(tree, options); - - expect(tree.exists('eslint.config.cjs')).toBeTruthy(); - expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot(); - expect(tree.exists('libs/test-lib/eslint.config.cjs')).toBeTruthy(); - expect( - tree.read('libs/test-lib/eslint.config.cjs', 'utf-8') - ).toMatchSnapshot(); - // check nx.json changes - const nxJson = readJson(tree, 'nx.json'); - expect(nxJson.targetDefaults.lint.inputs).toContain( - '{workspaceRoot}/eslint.config.cjs' - ); - expect(nxJson.namedInputs.production).toContain( - '!{projectRoot}/eslint.config.cjs' - ); - }); + it('should convert json successfully', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'cjs', + }); + await convertToFlatConfigGenerator(tree, options); - it('should add plugin extends', async () => { - await lintProjectGenerator(tree, { - skipFormat: false, - linter: Linter.EsLint, - project: 'test-lib', - setParserOptionsProject: false, + expect(tree.exists('eslint.config.cjs')).toBeTruthy(); + expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot(); + expect(tree.exists('libs/test-lib/eslint.config.cjs')).toBeTruthy(); + expect( + tree.read('libs/test-lib/eslint.config.cjs', 'utf-8') + ).toMatchSnapshot(); + // check nx.json changes + const nxJson = readJson(tree, 'nx.json'); + expect(nxJson.targetDefaults.lint.inputs).toContain( + '{workspaceRoot}/eslint.config.cjs' + ); + expect(nxJson.namedInputs.production).toContain( + '!{projectRoot}/eslint.config.cjs' + ); }); - updateJson(tree, '.eslintrc.json', (json) => { - json.extends = ['plugin:storybook/recommended']; - return json; + + it('should convert yaml successfully', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + eslintFilePatterns: ['**/*.ts'], + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'cjs', + }); + const yamlContent = dump(readJson(tree, 'libs/test-lib/.eslintrc.json')); + tree.delete('libs/test-lib/.eslintrc.json'); + tree.write('libs/test-lib/.eslintrc.yaml', yamlContent); + + await convertToFlatConfigGenerator(tree, options); + + expect(tree.exists('eslint.config.cjs')).toBeTruthy(); + expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot(); + expect(tree.exists('libs/test-lib/eslint.config.cjs')).toBeTruthy(); + expect( + tree.read('libs/test-lib/eslint.config.cjs', 'utf-8') + ).toMatchSnapshot(); + // check nx.json changes + const nxJson = readJson(tree, 'nx.json'); + expect(nxJson.targetDefaults.lint.inputs).toContain( + '{workspaceRoot}/eslint.config.cjs' + ); + expect(nxJson.namedInputs.production).toContain( + '!{projectRoot}/eslint.config.cjs' + ); }); - await convertToFlatConfigGenerator(tree, options); - expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchInlineSnapshot(` - "const { FlatCompat } = require('@eslint/eslintrc'); - const js = require('@eslint/js'); - const nxEslintPlugin = require('@nx/eslint-plugin'); + it('should convert yml successfully', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + eslintFilePatterns: ['**/*.ts'], + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'cjs', + }); + const yamlContent = dump(readJson(tree, 'libs/test-lib/.eslintrc.json')); + tree.delete('libs/test-lib/.eslintrc.json'); + tree.write('libs/test-lib/.eslintrc.yml', yamlContent); + + await convertToFlatConfigGenerator(tree, options); - const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, + expect(tree.exists('eslint.config.cjs')).toBeTruthy(); + expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot(); + expect(tree.exists('libs/test-lib/eslint.config.cjs')).toBeTruthy(); + expect( + tree.read('libs/test-lib/eslint.config.cjs', 'utf-8') + ).toMatchSnapshot(); + // check nx.json changes + const nxJson = readJson(tree, 'nx.json'); + expect(nxJson.targetDefaults.lint.inputs).toContain( + '{workspaceRoot}/eslint.config.cjs' + ); + expect(nxJson.namedInputs.production).toContain( + '!{projectRoot}/eslint.config.cjs' + ); + }); + + it('should add plugin extends', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'cjs', }); + updateJson(tree, '.eslintrc.json', (json) => { + json.extends = ['plugin:storybook/recommended']; + return json; + }); + await convertToFlatConfigGenerator(tree, options); - module.exports = [ - { - ignores: ['**/dist'], - }, - ...compat.extends('plugin:storybook/recommended'), - { plugins: { '@nx': nxEslintPlugin } }, - { - files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], - rules: { - '@nx/enforce-module-boundaries': [ - 'error', - { - enforceBuildableLibDependency: true, - allow: [], - depConstraints: [ - { - sourceTag: '*', - onlyDependOnLibsWithTags: ['*'], + expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchInlineSnapshot(` + "const { FlatCompat } = require('@eslint/eslintrc'); + const js = require('@eslint/js'); + const nxEslintPlugin = require('@nx/eslint-plugin'); + + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + + module.exports = [ + { + ignores: ['**/dist'], + }, + ...compat.extends('plugin:storybook/recommended'), + { plugins: { '@nx': nxEslintPlugin } }, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + rules: { + '@nx/enforce-module-boundaries': [ + 'error', + { + enforceBuildableLibDependency: true, + allow: [], + depConstraints: [ + { + sourceTag: '*', + onlyDependOnLibsWithTags: ['*'], + }, + ], + }, + ], }, - ], - }, - ], - }, - }, - ...compat - .config({ - extends: ['plugin:@nx/typescript'], - }) - .map((config) => ({ - ...config, - files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'], - rules: { - ...config.rules, - }, - })), - ...compat - .config({ - extends: ['plugin:@nx/javascript'], - }) - .map((config) => ({ - ...config, - files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'], - rules: { - ...config.rules, - }, - })), - ]; - " - `); - expect(tree.read('libs/test-lib/eslint.config.cjs', 'utf-8')) - .toMatchInlineSnapshot(` - "const baseConfig = require('../../eslint.config.cjs'); - - module.exports = [ - { - ignores: ['**/dist'], - }, - ...baseConfig, - { - files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], - // Override or add rules here - rules: {}, - }, - { - files: ['**/*.ts', '**/*.tsx'], - // Override or add rules here - rules: {}, - }, - { - files: ['**/*.js', '**/*.jsx'], - // Override or add rules here - rules: {}, - }, - ]; - " - `); - expect( - readJson(tree, 'package.json').devDependencies['@eslint/eslintrc'] - ).toEqual(eslintrcVersion); - }); + }, + ...compat + .config({ + extends: ['plugin:@nx/typescript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'], + rules: { + ...config.rules, + }, + })), + ...compat + .config({ + extends: ['plugin:@nx/javascript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'], + rules: { + ...config.rules, + }, + })), + ]; + " + `); + expect(tree.read('libs/test-lib/eslint.config.cjs', 'utf-8')) + .toMatchInlineSnapshot(` + "const baseConfig = require('../../eslint.config.cjs'); - it('should add global eslintignores', async () => { - await lintProjectGenerator(tree, { - skipFormat: false, - linter: Linter.EsLint, - project: 'test-lib', - setParserOptionsProject: false, + module.exports = [ + { + ignores: ['**/dist'], + }, + ...baseConfig, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + // Override or add rules here + rules: {}, + }, + { + files: ['**/*.ts', '**/*.tsx'], + // Override or add rules here + rules: {}, + }, + { + files: ['**/*.js', '**/*.jsx'], + // Override or add rules here + rules: {}, + }, + ]; + " + `); + expect( + readJson(tree, 'package.json').devDependencies['@eslint/eslintrc'] + ).toEqual(eslintrcVersion); }); - tree.write('.eslintignore', 'ignore/me'); - await convertToFlatConfigGenerator(tree, options); - const config = tree.read('eslint.config.cjs', 'utf-8'); - expect(config).toContain('ignore/me'); - expect(config).toMatchSnapshot(); - expect(tree.exists('.eslintignore')).toBeFalsy(); - }); + it('should add global eslintignores', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + }); + tree.write('.eslintignore', 'ignore/me'); + await convertToFlatConfigGenerator(tree, options); - it('should handle custom eslintignores', async () => { - await lintProjectGenerator(tree, { - skipFormat: false, - linter: Linter.EsLint, - project: 'test-lib', - setParserOptionsProject: false, - }); - tree.write('another-folder/.myeslintignore', 'ignore/me'); - updateJson(tree, 'libs/test-lib/project.json', (json) => { - json.targets.lint.options = json.targets.lint.options || {}; - json.targets.lint.options.ignorePath = 'another-folder/.myeslintignore'; - return json; + const config = tree.read('eslint.config.cjs', 'utf-8'); + expect(config).toContain('ignore/me'); + expect(config).toMatchSnapshot(); + expect(tree.exists('.eslintignore')).toBeFalsy(); }); - tree.write('libs/test-lib/.eslintignore', 'ignore/me/as/well'); - await convertToFlatConfigGenerator(tree, options); + it('should handle custom eslintignores', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'cjs', + }); + tree.write('another-folder/.myeslintignore', 'ignore/me'); + updateJson(tree, 'libs/test-lib/project.json', (json) => { + json.targets.lint.options = json.targets.lint.options || {}; + json.targets.lint.options.ignorePath = 'another-folder/.myeslintignore'; + return json; + }); + tree.write('libs/test-lib/.eslintignore', 'ignore/me/as/well'); - expect( - tree.read('libs/test-lib/eslint.config.cjs', 'utf-8') - ).toMatchSnapshot(); - expect(tree.exists('another-folder/.myeslintignore')).toBeFalsy(); - expect(tree.exists('libs/test-lib/.eslintignore')).toBeFalsy(); + await convertToFlatConfigGenerator(tree, options); - expect( - readJson(tree, 'libs/test-lib/project.json').targets.lint.options - .ignorePath - ).toBeUndefined(); - }); + expect( + tree.read('libs/test-lib/eslint.config.cjs', 'utf-8') + ).toMatchSnapshot(); + expect(tree.exists('another-folder/.myeslintignore')).toBeFalsy(); + expect(tree.exists('libs/test-lib/.eslintignore')).toBeFalsy(); - it('should add settings', async () => { - await lintProjectGenerator(tree, { - skipFormat: false, - linter: Linter.EsLint, - project: 'test-lib', - setParserOptionsProject: false, + expect( + readJson(tree, 'libs/test-lib/project.json').targets.lint.options + .ignorePath + ).toBeUndefined(); }); - updateJson(tree, '.eslintrc.json', (json) => { - json.settings = { - sharedData: 'Hello', - }; - return json; + + it('should add settings', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'cjs', + }); + updateJson(tree, '.eslintrc.json', (json) => { + json.settings = { + sharedData: 'Hello', + }; + return json; + }); + await convertToFlatConfigGenerator(tree, options); + + expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot(); }); - await convertToFlatConfigGenerator(tree, options); - expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot(); - }); + it('should add env configuration', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'cjs', + }); + updateJson(tree, '.eslintrc.json', (json) => { + json.env = { + browser: true, + node: true, + }; + return json; + }); + await convertToFlatConfigGenerator(tree, options); - it('should add env configuration', async () => { - await lintProjectGenerator(tree, { - skipFormat: false, - linter: Linter.EsLint, - project: 'test-lib', - setParserOptionsProject: false, - }); - updateJson(tree, '.eslintrc.json', (json) => { - json.env = { - browser: true, - node: true, - }; - return json; + expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot(); }); - await convertToFlatConfigGenerator(tree, options); - expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot(); - }); + it('should add global configuration', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'cjs', + }); + updateJson(tree, '.eslintrc.json', (json) => { + json.globals = { + myCustomGlobal: 'readonly', + }; + return json; + }); + await convertToFlatConfigGenerator(tree, options); - it('should add global configuration', async () => { - await lintProjectGenerator(tree, { - skipFormat: false, - linter: Linter.EsLint, - project: 'test-lib', - setParserOptionsProject: false, + expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot(); }); - updateJson(tree, '.eslintrc.json', (json) => { - json.globals = { - myCustomGlobal: 'readonly', - }; - return json; + + it('should add global and env configuration', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'cjs', + }); + updateJson(tree, '.eslintrc.json', (json) => { + json.globals = { + myCustomGlobal: 'readonly', + }; + json.env = { + browser: true, + }; + return json; + }); + await convertToFlatConfigGenerator(tree, options); + + expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot(); }); - await convertToFlatConfigGenerator(tree, options); - expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot(); - }); + it('should add plugins', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'cjs', + }); + updateJson(tree, '.eslintrc.json', (json) => { + json.plugins = [ + 'eslint-plugin-import', + 'single-name', + '@scope/with-name', + '@just-scope', + ]; + return json; + }); + await convertToFlatConfigGenerator(tree, options); - it('should add global and env configuration', async () => { - await lintProjectGenerator(tree, { - skipFormat: false, - linter: Linter.EsLint, - project: 'test-lib', - setParserOptionsProject: false, + expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot(); }); - updateJson(tree, '.eslintrc.json', (json) => { - json.globals = { - myCustomGlobal: 'readonly', - }; - json.env = { - browser: true, - }; - return json; + + it('should add parser', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'cjs', + }); + updateJson(tree, '.eslintrc.json', (json) => { + json.parser = '@typescript-eslint/parser'; + return json; + }); + await convertToFlatConfigGenerator(tree, options); + + expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot(); }); - await convertToFlatConfigGenerator(tree, options); - expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot(); - }); + it('should add linter options', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'cjs', + }); + updateJson(tree, '.eslintrc.json', (json) => { + json.noInlineConfig = true; + return json; + }); + await convertToFlatConfigGenerator(tree, options); - it('should add plugins', async () => { - await lintProjectGenerator(tree, { - skipFormat: false, - linter: Linter.EsLint, - project: 'test-lib', - setParserOptionsProject: false, - }); - updateJson(tree, '.eslintrc.json', (json) => { - json.plugins = [ - 'eslint-plugin-import', - 'single-name', - '@scope/with-name', - '@just-scope', - ]; - return json; + expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchInlineSnapshot(` + "const { FlatCompat } = require('@eslint/eslintrc'); + const js = require('@eslint/js'); + const nxEslintPlugin = require('@nx/eslint-plugin'); + + const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + }); + + module.exports = [ + { + ignores: ['**/dist'], + }, + { plugins: { '@nx': nxEslintPlugin } }, + { + linterOptions: { + noInlineConfig: true, + }, + }, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + rules: { + '@nx/enforce-module-boundaries': [ + 'error', + { + enforceBuildableLibDependency: true, + allow: [], + depConstraints: [ + { + sourceTag: '*', + onlyDependOnLibsWithTags: ['*'], + }, + ], + }, + ], + }, + }, + ...compat + .config({ + extends: ['plugin:@nx/typescript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'], + rules: { + ...config.rules, + }, + })), + ...compat + .config({ + extends: ['plugin:@nx/javascript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'], + rules: { + ...config.rules, + }, + })), + ]; + " + `); }); - await convertToFlatConfigGenerator(tree, options); - expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot(); - }); + it('should convert project if target is defined via plugin as string', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'cjs', + }); + updateJson(tree, 'nx.json', (json: NxJsonConfiguration) => { + delete json.targetDefaults; + json.plugins = ['@nx/eslint/plugin']; + return json; + }); + updateJson( + tree, + 'libs/test-lib/project.json', + (json: ProjectConfiguration) => { + delete json.targets.lint; + return json; + } + ); - it('should add parser', async () => { - await lintProjectGenerator(tree, { - skipFormat: false, - linter: Linter.EsLint, - project: 'test-lib', - setParserOptionsProject: false, + expect(tree.exists('eslint.config.cjs')).toBeFalsy(); + expect(tree.exists('libs/test-lib/eslint.config.cjs')).toBeFalsy(); + await convertToFlatConfigGenerator(tree, options); + expect(tree.exists('eslint.config.cjs')).toBeTruthy(); + expect(tree.exists('libs/test-lib/eslint.config.cjs')).toBeTruthy(); }); - updateJson(tree, '.eslintrc.json', (json) => { - json.parser = '@typescript-eslint/parser'; - return json; + + it('should convert project if target is defined via plugin as object', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'cjs', + }); + updateJson(tree, 'nx.json', (json: NxJsonConfiguration) => { + delete json.targetDefaults; + json.plugins = [ + { + plugin: '@nx/eslint/plugin', + options: { + targetName: 'lint', + }, + }, + ]; + return json; + }); + updateJson( + tree, + 'libs/test-lib/project.json', + (json: ProjectConfiguration) => { + delete json.targets.lint; + return json; + } + ); + + expect(tree.exists('eslint.config.cjs')).toBeFalsy(); + expect(tree.exists('libs/test-lib/eslint.config.cjs')).toBeFalsy(); + await convertToFlatConfigGenerator(tree, options); + expect(tree.exists('eslint.config.cjs')).toBeTruthy(); + expect(tree.exists('libs/test-lib/eslint.config.cjs')).toBeTruthy(); }); - await convertToFlatConfigGenerator(tree, options); - expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchSnapshot(); + it('should handle parser options even if parser is extended', async () => { + addProjectConfiguration(tree, 'dx-assets-ui', { + root: 'apps/dx-assets-ui', + targets: {}, + }); + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + + project: 'dx-assets-ui', + setParserOptionsProject: false, + eslintConfigFormat: 'cjs', + }); + updateJson(tree, 'apps/dx-assets-ui/.eslintrc.json', () => { + return { + extends: ['../../.eslintrc.json'], + ignorePatterns: ['!**/*', '__fixtures__/**/*'], + overrides: [ + { + files: ['*.ts', '*.tsx', '*.js', '*.jsx'], + parserOptions: { + project: ['apps/dx-assets-ui/tsconfig.*?.json'], + }, + rules: {}, + }, + { + files: ['*.ts', '*.tsx'], + rules: {}, + }, + { + files: ['*.js', '*.jsx'], + rules: {}, + }, + ], + }; + }); + + await convertToFlatConfigGenerator(tree, options); + expect(tree.exists('apps/dx-assets-ui/eslint.config.cjs')).toBeTruthy(); + expect(tree.exists('eslint.config.cjs')).toBeTruthy(); + expect(tree.read('apps/dx-assets-ui/eslint.config.cjs', 'utf-8')) + .toMatchInlineSnapshot(` + "const baseConfig = require('../../eslint.config.cjs'); + + module.exports = [ + { + ignores: ['**/dist'], + }, + ...baseConfig, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + // Override or add rules here + rules: {}, + languageOptions: { + parserOptions: { + project: ['apps/dx-assets-ui/tsconfig.*?.json'], + }, + }, + }, + { + files: ['**/*.ts', '**/*.tsx'], + // Override or add rules here + rules: {}, + }, + { + files: ['**/*.js', '**/*.jsx'], + // Override or add rules here + rules: {}, + }, + { + ignores: ['__fixtures__/**/*'], + }, + ]; + " + `); + }); }); - it('should add linter options', async () => { - await lintProjectGenerator(tree, { + describe('MJS', () => { + const options: ConvertToFlatConfigGeneratorSchema = { skipFormat: false, - linter: Linter.EsLint, - project: 'test-lib', - setParserOptionsProject: false, + eslintConfigFormat: 'mjs', + }; + + it('should update dependencies', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'mjs', + }); + await convertToFlatConfigGenerator(tree, options); + + expect(tree.read('package.json', 'utf-8')).toMatchInlineSnapshot(` + "{ + "name": "@proj/source", + "dependencies": {}, + "devDependencies": { + "@eslint/eslintrc": "^2.1.1", + "@nx/eslint": "0.0.1", + "@nx/eslint-plugin": "0.0.1", + "eslint": "^9.8.0", + "eslint-config-prettier": "^9.0.0", + "typescript-eslint": "^8.13.0" + } + } + " + `); }); - updateJson(tree, '.eslintrc.json', (json) => { - json.noInlineConfig = true; - return json; + + it('should convert json successfully', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'mjs', + }); + await convertToFlatConfigGenerator(tree, options); + + expect(tree.exists('eslint.config.mjs')).toBeTruthy(); + expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchSnapshot(); + expect(tree.exists('libs/test-lib/eslint.config.mjs')).toBeTruthy(); + expect( + tree.read('libs/test-lib/eslint.config.mjs', 'utf-8') + ).toMatchSnapshot(); + // check nx.json changes + const nxJson = readJson(tree, 'nx.json'); + expect(nxJson.targetDefaults.lint.inputs).toContain( + '{workspaceRoot}/eslint.config.mjs' + ); + expect(nxJson.namedInputs.production).toContain( + '!{projectRoot}/eslint.config.mjs' + ); }); - await convertToFlatConfigGenerator(tree, options); - expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchInlineSnapshot(` - "const { FlatCompat } = require('@eslint/eslintrc'); - const js = require('@eslint/js'); - const nxEslintPlugin = require('@nx/eslint-plugin'); + it('should convert yaml successfully', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + eslintFilePatterns: ['**/*.ts'], + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'mjs', + }); + const yamlContent = dump(readJson(tree, 'libs/test-lib/.eslintrc.json')); + tree.delete('libs/test-lib/.eslintrc.json'); + tree.write('libs/test-lib/.eslintrc.yaml', yamlContent); + + await convertToFlatConfigGenerator(tree, options); + + expect(tree.exists('eslint.config.mjs')).toBeTruthy(); + expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchSnapshot(); + expect(tree.exists('libs/test-lib/eslint.config.mjs')).toBeTruthy(); + expect( + tree.read('libs/test-lib/eslint.config.mjs', 'utf-8') + ).toMatchSnapshot(); + // check nx.json changes + const nxJson = readJson(tree, 'nx.json'); + expect(nxJson.targetDefaults.lint.inputs).toContain( + '{workspaceRoot}/eslint.config.mjs' + ); + expect(nxJson.namedInputs.production).toContain( + '!{projectRoot}/eslint.config.mjs' + ); + }); - const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, + it('should convert yml successfully', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + eslintFilePatterns: ['**/*.ts'], + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'mjs', }); + const yamlContent = dump(readJson(tree, 'libs/test-lib/.eslintrc.json')); + tree.delete('libs/test-lib/.eslintrc.json'); + tree.write('libs/test-lib/.eslintrc.yml', yamlContent); - module.exports = [ - { - ignores: ['**/dist'], - }, - { plugins: { '@nx': nxEslintPlugin } }, - { - linterOptions: { - noInlineConfig: true, - }, - }, - { - files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], - rules: { - '@nx/enforce-module-boundaries': [ - 'error', - { - enforceBuildableLibDependency: true, - allow: [], - depConstraints: [ - { - sourceTag: '*', - onlyDependOnLibsWithTags: ['*'], - }, - ], - }, - ], + await convertToFlatConfigGenerator(tree, options); + + expect(tree.exists('eslint.config.mjs')).toBeTruthy(); + expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchSnapshot(); + expect(tree.exists('libs/test-lib/eslint.config.mjs')).toBeTruthy(); + expect( + tree.read('libs/test-lib/eslint.config.mjs', 'utf-8') + ).toMatchSnapshot(); + // check nx.json changes + const nxJson = readJson(tree, 'nx.json'); + expect(nxJson.targetDefaults.lint.inputs).toContain( + '{workspaceRoot}/eslint.config.mjs' + ); + expect(nxJson.namedInputs.production).toContain( + '!{projectRoot}/eslint.config.mjs' + ); + }); + + it('should add plugin extends', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'mjs', + }); + updateJson(tree, '.eslintrc.json', (json) => { + json.extends = ['plugin:storybook/recommended']; + return json; + }); + await convertToFlatConfigGenerator(tree, options); + + expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchInlineSnapshot(` + "import { FlatCompat } from '@eslint/eslintrc'; + import { dirname } from 'path'; + import { fileURLToPath } from 'url'; + import js from '@eslint/js'; + import nxEslintPlugin from '@nx/eslint-plugin'; + + const compat = new FlatCompat({ + baseDirectory: dirname(fileURLToPath(import.meta.url)), + recommendedConfig: js.configs.recommended, + }); + + export default [ + { + ignores: ['**/dist'], }, - }, - ...compat - .config({ - extends: ['plugin:@nx/typescript'], - }) - .map((config) => ({ - ...config, - files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'], - rules: { - ...config.rules, - }, - })), - ...compat - .config({ - extends: ['plugin:@nx/javascript'], - }) - .map((config) => ({ - ...config, - files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'], + ...compat.extends('plugin:storybook/recommended'), + { plugins: { '@nx': nxEslintPlugin } }, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], rules: { - ...config.rules, + '@nx/enforce-module-boundaries': [ + 'error', + { + enforceBuildableLibDependency: true, + allow: [], + depConstraints: [ + { + sourceTag: '*', + onlyDependOnLibsWithTags: ['*'], + }, + ], + }, + ], }, - })), - ]; - " - `); - }); + }, + ...compat + .config({ + extends: ['plugin:@nx/typescript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'], + rules: { + ...config.rules, + }, + })), + ...compat + .config({ + extends: ['plugin:@nx/javascript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'], + rules: { + ...config.rules, + }, + })), + ]; + " + `); + expect(tree.read('libs/test-lib/eslint.config.mjs', 'utf-8')) + .toMatchInlineSnapshot(` + "import baseConfig from '../../eslint.config.mjs'; - it('should convert project if target is defined via plugin as string', async () => { - await lintProjectGenerator(tree, { - skipFormat: false, - linter: Linter.EsLint, - project: 'test-lib', - setParserOptionsProject: false, + export default [ + { + ignores: ['**/dist'], + }, + ...baseConfig, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + // Override or add rules here + rules: {}, + }, + { + files: ['**/*.ts', '**/*.tsx'], + // Override or add rules here + rules: {}, + }, + { + files: ['**/*.js', '**/*.jsx'], + // Override or add rules here + rules: {}, + }, + ]; + " + `); + expect( + readJson(tree, 'package.json').devDependencies['@eslint/eslintrc'] + ).toEqual(eslintrcVersion); }); - updateJson(tree, 'nx.json', (json: NxJsonConfiguration) => { - delete json.targetDefaults; - json.plugins = ['@nx/eslint/plugin']; - return json; + + it('should add global eslintignores', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + }); + tree.write('.eslintignore', 'ignore/me'); + await convertToFlatConfigGenerator(tree, options); + + const config = tree.read('eslint.config.mjs', 'utf-8'); + expect(config).toContain('ignore/me'); + expect(config).toMatchSnapshot(); + expect(tree.exists('.eslintignore')).toBeFalsy(); }); - updateJson( - tree, - 'libs/test-lib/project.json', - (json: ProjectConfiguration) => { - delete json.targets.lint; + + it('should handle custom eslintignores', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'mjs', + }); + tree.write('another-folder/.myeslintignore', 'ignore/me'); + updateJson(tree, 'libs/test-lib/project.json', (json) => { + json.targets.lint.options = json.targets.lint.options || {}; + json.targets.lint.options.ignorePath = 'another-folder/.myeslintignore'; return json; - } - ); - - expect(tree.exists('eslint.config.cjs')).toBeFalsy(); - expect(tree.exists('libs/test-lib/eslint.config.cjs')).toBeFalsy(); - await convertToFlatConfigGenerator(tree, options); - expect(tree.exists('eslint.config.cjs')).toBeTruthy(); - expect(tree.exists('libs/test-lib/eslint.config.cjs')).toBeTruthy(); - }); + }); + tree.write('libs/test-lib/.eslintignore', 'ignore/me/as/well'); - it('should convert project if target is defined via plugin as object', async () => { - await lintProjectGenerator(tree, { - skipFormat: false, - linter: Linter.EsLint, - project: 'test-lib', - setParserOptionsProject: false, + await convertToFlatConfigGenerator(tree, options); + + expect( + tree.read('libs/test-lib/eslint.config.mjs', 'utf-8') + ).toMatchSnapshot(); + expect(tree.exists('another-folder/.myeslintignore')).toBeFalsy(); + expect(tree.exists('libs/test-lib/.eslintignore')).toBeFalsy(); + + expect( + readJson(tree, 'libs/test-lib/project.json').targets.lint.options + .ignorePath + ).toBeUndefined(); }); - updateJson(tree, 'nx.json', (json: NxJsonConfiguration) => { - delete json.targetDefaults; - json.plugins = [ - { - plugin: '@nx/eslint/plugin', - options: { - targetName: 'lint', - }, - }, - ]; - return json; + + it('should add settings', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'mjs', + }); + updateJson(tree, '.eslintrc.json', (json) => { + json.settings = { + sharedData: 'Hello', + }; + return json; + }); + await convertToFlatConfigGenerator(tree, options); + + expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchSnapshot(); }); - updateJson( - tree, - 'libs/test-lib/project.json', - (json: ProjectConfiguration) => { - delete json.targets.lint; + + it('should add env configuration', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'mjs', + }); + updateJson(tree, '.eslintrc.json', (json) => { + json.env = { + browser: true, + node: true, + }; return json; - } - ); - - expect(tree.exists('eslint.config.cjs')).toBeFalsy(); - expect(tree.exists('libs/test-lib/eslint.config.cjs')).toBeFalsy(); - await convertToFlatConfigGenerator(tree, options); - expect(tree.exists('eslint.config.cjs')).toBeTruthy(); - expect(tree.exists('libs/test-lib/eslint.config.cjs')).toBeTruthy(); - }); + }); + await convertToFlatConfigGenerator(tree, options); - it('should handle parser options even if parser is extended', async () => { - addProjectConfiguration(tree, 'dx-assets-ui', { - root: 'apps/dx-assets-ui', - targets: {}, + expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchSnapshot(); }); - await lintProjectGenerator(tree, { - skipFormat: false, - linter: Linter.EsLint, - project: 'dx-assets-ui', - setParserOptionsProject: false, + it('should add global configuration', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'mjs', + }); + updateJson(tree, '.eslintrc.json', (json) => { + json.globals = { + myCustomGlobal: 'readonly', + }; + return json; + }); + await convertToFlatConfigGenerator(tree, options); + + expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchSnapshot(); }); - updateJson(tree, 'apps/dx-assets-ui/.eslintrc.json', () => { - return { - extends: ['../../.eslintrc.json'], - ignorePatterns: ['!**/*', '__fixtures__/**/*'], - overrides: [ + + it('should add global and env configuration', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'mjs', + }); + updateJson(tree, '.eslintrc.json', (json) => { + json.globals = { + myCustomGlobal: 'readonly', + }; + json.env = { + browser: true, + }; + return json; + }); + await convertToFlatConfigGenerator(tree, options); + + expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchSnapshot(); + }); + + it('should add plugins', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'mjs', + }); + updateJson(tree, '.eslintrc.json', (json) => { + json.plugins = [ + 'eslint-plugin-import', + 'single-name', + '@scope/with-name', + '@just-scope', + ]; + return json; + }); + await convertToFlatConfigGenerator(tree, options); + + expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchSnapshot(); + }); + + it('should add parser', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'mjs', + }); + updateJson(tree, '.eslintrc.json', (json) => { + json.parser = '@typescript-eslint/parser'; + return json; + }); + await convertToFlatConfigGenerator(tree, options); + + expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchSnapshot(); + }); + + it('should add linter options', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'mjs', + }); + updateJson(tree, '.eslintrc.json', (json) => { + json.noInlineConfig = true; + return json; + }); + await convertToFlatConfigGenerator(tree, options); + + expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchInlineSnapshot(` + "import { FlatCompat } from '@eslint/eslintrc'; + import { dirname } from 'path'; + import { fileURLToPath } from 'url'; + import js from '@eslint/js'; + import nxEslintPlugin from '@nx/eslint-plugin'; + + const compat = new FlatCompat({ + baseDirectory: dirname(fileURLToPath(import.meta.url)), + recommendedConfig: js.configs.recommended, + }); + + export default [ + { + ignores: ['**/dist'], + }, + { plugins: { '@nx': nxEslintPlugin } }, { - files: ['*.ts', '*.tsx', '*.js', '*.jsx'], - parserOptions: { - project: ['apps/dx-assets-ui/tsconfig.*?.json'], + linterOptions: { + noInlineConfig: true, }, - rules: {}, }, { - files: ['*.ts', '*.tsx'], - rules: {}, + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + rules: { + '@nx/enforce-module-boundaries': [ + 'error', + { + enforceBuildableLibDependency: true, + allow: [], + depConstraints: [ + { + sourceTag: '*', + onlyDependOnLibsWithTags: ['*'], + }, + ], + }, + ], + }, }, + ...compat + .config({ + extends: ['plugin:@nx/typescript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'], + rules: { + ...config.rules, + }, + })), + ...compat + .config({ + extends: ['plugin:@nx/javascript'], + }) + .map((config) => ({ + ...config, + files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'], + rules: { + ...config.rules, + }, + })), + ]; + " + `); + }); + + it('should convert project if target is defined via plugin as string', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'mjs', + }); + updateJson(tree, 'nx.json', (json: NxJsonConfiguration) => { + delete json.targetDefaults; + json.plugins = ['@nx/eslint/plugin']; + return json; + }); + updateJson( + tree, + 'libs/test-lib/project.json', + (json: ProjectConfiguration) => { + delete json.targets.lint; + return json; + } + ); + + expect(tree.exists('eslint.config.mjs')).toBeFalsy(); + expect(tree.exists('libs/test-lib/eslint.config.mjs')).toBeFalsy(); + await convertToFlatConfigGenerator(tree, options); + expect(tree.exists('eslint.config.mjs')).toBeTruthy(); + expect(tree.exists('libs/test-lib/eslint.config.mjs')).toBeTruthy(); + }); + + it('should convert project if target is defined via plugin as object', async () => { + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + eslintConfigFormat: 'mjs', + }); + updateJson(tree, 'nx.json', (json: NxJsonConfiguration) => { + delete json.targetDefaults; + json.plugins = [ { - files: ['*.js', '*.jsx'], - rules: {}, + plugin: '@nx/eslint/plugin', + options: { + targetName: 'lint', + }, }, - ], - }; + ]; + return json; + }); + updateJson( + tree, + 'libs/test-lib/project.json', + (json: ProjectConfiguration) => { + delete json.targets.lint; + return json; + } + ); + + expect(tree.exists('eslint.config.mjs')).toBeFalsy(); + expect(tree.exists('libs/test-lib/eslint.config.mjs')).toBeFalsy(); + await convertToFlatConfigGenerator(tree, options); + expect(tree.exists('eslint.config.mjs')).toBeTruthy(); + expect(tree.exists('libs/test-lib/eslint.config.mjs')).toBeTruthy(); }); - await convertToFlatConfigGenerator(tree, options); - expect(tree.exists('apps/dx-assets-ui/eslint.config.cjs')).toBeTruthy(); - expect(tree.exists('eslint.config.cjs')).toBeTruthy(); - expect(tree.read('apps/dx-assets-ui/eslint.config.cjs', 'utf-8')) - .toMatchInlineSnapshot(` - "const baseConfig = require('../../eslint.config.cjs'); + it('should handle parser options even if parser is extended', async () => { + addProjectConfiguration(tree, 'dx-assets-ui', { + root: 'apps/dx-assets-ui', + targets: {}, + }); + await lintProjectGenerator(tree, { + skipFormat: false, + linter: Linter.EsLint, - module.exports = [ - { - ignores: ['**/dist'], - }, - ...baseConfig, - { - files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], - // Override or add rules here - rules: {}, - languageOptions: { - parserOptions: { - project: ['apps/dx-assets-ui/tsconfig.*?.json'], + project: 'dx-assets-ui', + setParserOptionsProject: false, + eslintConfigFormat: 'mjs', + }); + updateJson(tree, 'apps/dx-assets-ui/.eslintrc.json', () => { + return { + extends: ['../../.eslintrc.json'], + ignorePatterns: ['!**/*', '__fixtures__/**/*'], + overrides: [ + { + files: ['*.ts', '*.tsx', '*.js', '*.jsx'], + parserOptions: { + project: ['apps/dx-assets-ui/tsconfig.*?.json'], + }, + rules: {}, + }, + { + files: ['*.ts', '*.tsx'], + rules: {}, }, + { + files: ['*.js', '*.jsx'], + rules: {}, + }, + ], + }; + }); + + await convertToFlatConfigGenerator(tree, options); + expect(tree.exists('apps/dx-assets-ui/eslint.config.mjs')).toBeTruthy(); + expect(tree.exists('eslint.config.mjs')).toBeTruthy(); + expect(tree.read('apps/dx-assets-ui/eslint.config.mjs', 'utf-8')) + .toMatchInlineSnapshot(` + "import baseConfig from '../../eslint.config.mjs'; + + export default [ + { + ignores: ['**/dist'], }, - }, - { - files: ['**/*.ts', '**/*.tsx'], - // Override or add rules here - rules: {}, - }, - { - files: ['**/*.js', '**/*.jsx'], - // Override or add rules here - rules: {}, - }, - { - ignores: ['__fixtures__/**/*'], - }, - ]; - " - `); + ...baseConfig, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + // Override or add rules here + rules: {}, + languageOptions: { + parserOptions: { + project: ['apps/dx-assets-ui/tsconfig.*?.json'], + }, + }, + }, + { + files: ['**/*.ts', '**/*.tsx'], + // Override or add rules here + rules: {}, + }, + { + files: ['**/*.js', '**/*.jsx'], + // Override or add rules here + rules: {}, + }, + { + ignores: ['__fixtures__/**/*'], + }, + ]; + " + `); + }); }); }); diff --git a/packages/eslint/src/generators/convert-to-flat-config/generator.ts b/packages/eslint/src/generators/convert-to-flat-config/generator.ts index 65ae50940786f..5fc0e10c45d97 100644 --- a/packages/eslint/src/generators/convert-to-flat-config/generator.ts +++ b/packages/eslint/src/generators/convert-to-flat-config/generator.ts @@ -39,11 +39,12 @@ export async function convertToFlatConfigGenerator( 'Only json and yaml eslint config files are supported for conversion' ); } + options.eslintConfigFormat ??= 'mjs'; const eslintIgnoreFiles = new Set(['.eslintignore']); - // convert root eslint config to eslint.config.cjs - convertRootToFlatConfig(tree, eslintFile); + // convert root eslint config to eslint.config.cjs or eslint.base.config.mjs based on eslintConfigFormat + convertRootToFlatConfig(tree, eslintFile, options.eslintConfigFormat); // convert project eslint files to eslint.config.cjs const projects = getProjects(tree); @@ -53,7 +54,8 @@ export async function convertToFlatConfigGenerator( project, projectConfig, readNxJson(tree), - eslintIgnoreFiles + eslintIgnoreFiles, + options.eslintConfigFormat ); } @@ -63,7 +65,7 @@ export async function convertToFlatConfigGenerator( } // replace references in nx.json - updateNxJsonConfig(tree); + updateNxJsonConfig(tree, options.eslintConfigFormat); // install missing packages if (!options.skipFormat) { @@ -75,15 +77,26 @@ export async function convertToFlatConfigGenerator( export default convertToFlatConfigGenerator; -function convertRootToFlatConfig(tree: Tree, eslintFile: string) { +function convertRootToFlatConfig( + tree: Tree, + eslintFile: string, + format: 'cjs' | 'mjs' +) { if (/\.base\.(js|json|yml|yaml)$/.test(eslintFile)) { - convertConfigToFlatConfig(tree, '', eslintFile, 'eslint.base.config.mjs'); + convertConfigToFlatConfig( + tree, + '', + eslintFile, + `eslint.base.config.${format}`, + format + ); } convertConfigToFlatConfig( tree, '', eslintFile.replace('.base.', '.'), - 'eslint.config.mjs' + `eslint.config.${format}`, + format ); } @@ -92,7 +105,8 @@ function convertProjectToFlatConfig( project: string, projectConfig: ProjectConfiguration, nxJson: NxJsonConfiguration, - eslintIgnoreFiles: Set + eslintIgnoreFiles: Set, + format: 'cjs' | 'mjs' ) { const eslintFile = findEslintFile(tree, projectConfig.root); if (eslintFile && !eslintFile.endsWith('.js')) { @@ -132,7 +146,8 @@ function convertProjectToFlatConfig( tree, projectConfig.root, eslintFile, - 'eslint.config.cjs', + `eslint.config.${format}`, + format, ignorePath ); eslintIgnoreFiles.add(`${projectConfig.root}/.eslintignore`); @@ -146,22 +161,22 @@ function convertProjectToFlatConfig( // update names of eslint files in nx.json // and remove eslintignore -function updateNxJsonConfig(tree: Tree) { +function updateNxJsonConfig(tree: Tree, format: 'cjs' | 'mjs') { if (tree.exists('nx.json')) { updateJson(tree, 'nx.json', (json: NxJsonConfiguration) => { if (json.targetDefaults?.lint?.inputs) { const inputSet = new Set(json.targetDefaults.lint.inputs); - inputSet.add('{workspaceRoot}/eslint.config.cjs'); + inputSet.add(`{workspaceRoot}/eslint.config.${format}`); json.targetDefaults.lint.inputs = Array.from(inputSet); } if (json.targetDefaults?.['@nx/eslint:lint']?.inputs) { const inputSet = new Set(json.targetDefaults['@nx/eslint:lint'].inputs); - inputSet.add('{workspaceRoot}/eslint.config.cjs'); + inputSet.add(`{workspaceRoot}/eslint.config.${format}`); json.targetDefaults['@nx/eslint:lint'].inputs = Array.from(inputSet); } if (json.namedInputs?.production) { const inputSet = new Set(json.namedInputs.production); - inputSet.add('!{projectRoot}/eslint.config.cjs'); + inputSet.add(`!{projectRoot}/eslint.config.${format}`); json.namedInputs.production = Array.from(inputSet); } return json; @@ -174,6 +189,7 @@ function convertConfigToFlatConfig( root: string, source: string, target: string, + format: 'cjs' | 'mjs', ignorePath?: string ) { const ignorePaths = ignorePath @@ -186,7 +202,8 @@ function convertConfigToFlatConfig( tree, root, config, - ignorePaths + ignorePaths, + format ); return processConvertedConfig(tree, root, source, target, conversionResult); } @@ -201,7 +218,8 @@ function convertConfigToFlatConfig( tree, root, config, - ignorePaths + ignorePaths, + format ); return processConvertedConfig(tree, root, source, target, conversionResult); } diff --git a/packages/eslint/src/generators/convert-to-flat-config/schema.d.ts b/packages/eslint/src/generators/convert-to-flat-config/schema.d.ts index 67eeb4ddadf42..a61f5db080624 100644 --- a/packages/eslint/src/generators/convert-to-flat-config/schema.d.ts +++ b/packages/eslint/src/generators/convert-to-flat-config/schema.d.ts @@ -1,3 +1,4 @@ export interface ConvertToFlatConfigGeneratorSchema { skipFormat?: boolean; + eslintConfigFormat?: 'mjs' | 'cjs'; } diff --git a/packages/eslint/src/generators/convert-to-flat-config/schema.json b/packages/eslint/src/generators/convert-to-flat-config/schema.json index f738885e50834..22f38cd3b3307 100644 --- a/packages/eslint/src/generators/convert-to-flat-config/schema.json +++ b/packages/eslint/src/generators/convert-to-flat-config/schema.json @@ -10,6 +10,12 @@ "description": "Skip formatting files.", "default": false, "x-priority": "internal" + }, + "eslintConfigFormat": { + "type": "string", + "description": "The format of the ESLint configuration file", + "enum": ["cjs", "mjs"], + "default": "mjs" } }, "additionalProperties": false, diff --git a/packages/eslint/src/generators/convert-to-inferred/convert-to-inferred.ts b/packages/eslint/src/generators/convert-to-inferred/convert-to-inferred.ts index c50ea644554b4..6825b1f179df7 100644 --- a/packages/eslint/src/generators/convert-to-inferred/convert-to-inferred.ts +++ b/packages/eslint/src/generators/convert-to-inferred/convert-to-inferred.ts @@ -66,6 +66,7 @@ function postTargetTransformer( '{workspaceRoot}/.eslintrc.json', '{workspaceRoot}/.eslintignore', '{workspaceRoot}/eslint.config.cjs', + '{workspaceRoot}/eslint.config.mjs', ].includes(input) ); if (inputs.length === 0) { diff --git a/packages/eslint/src/generators/init/global-eslint-config.ts b/packages/eslint/src/generators/init/global-eslint-config.ts index e384918a2d40a..122cedc3e5b18 100644 --- a/packages/eslint/src/generators/init/global-eslint-config.ts +++ b/packages/eslint/src/generators/init/global-eslint-config.ts @@ -92,9 +92,10 @@ export const getGlobalEsLintConfiguration = ( }; export const getGlobalFlatEslintConfiguration = ( + format: 'cjs' | 'mjs', rootProject?: boolean ): string => { - const nodeList = createNodeList(new Map(), []); + const nodeList = createNodeList(new Map(), [], format); let content = stringifyNodeList(nodeList); content = addImportToFlatConfig(content, 'nx', '@nx/eslint-plugin'); @@ -114,49 +115,58 @@ export const getGlobalFlatEslintConfiguration = ( content = addBlockToFlatConfigExport( content, - generateFlatOverride({ - ignores: ['**/dist'], - }) + generateFlatOverride( + { + ignores: ['**/dist'], + }, + format + ) ); if (!rootProject) { content = addBlockToFlatConfigExport( content, - generateFlatOverride({ - files: ['*.ts', '*.tsx', '*.js', '*.jsx'], - rules: { - '@nx/enforce-module-boundaries': [ - 'error', - { - enforceBuildableLibDependency: true, - allow: [ - // This allows a root project to be present without causing lint errors - // since all projects will depend on this base file. - '^.*/eslint(\\.base)?\\.config\\.[cm]?js$', - ], - depConstraints: [ - { sourceTag: '*', onlyDependOnLibsWithTags: ['*'] }, - ], - }, - ], - } as Linter.RulesRecord, - }) + generateFlatOverride( + { + files: ['*.ts', '*.tsx', '*.js', '*.jsx'], + rules: { + '@nx/enforce-module-boundaries': [ + 'error', + { + enforceBuildableLibDependency: true, + allow: [ + // This allows a root project to be present without causing lint errors + // since all projects will depend on this base file. + '^.*/eslint(\\.base)?\\.config\\.[cm]?js$', + ], + depConstraints: [ + { sourceTag: '*', onlyDependOnLibsWithTags: ['*'] }, + ], + }, + ], + } as Linter.RulesRecord, + }, + format + ) ); } content = addBlockToFlatConfigExport( content, - generateFlatOverride({ - files: [ - '**/*.ts', - '**/*.tsx', - '**/*.js', - '**/*.jsx', - '**/*.cjs', - '**/*.mjs', - ], - rules: {}, - }) + generateFlatOverride( + { + files: [ + '**/*.ts', + '**/*.tsx', + '**/*.js', + '**/*.jsx', + '**/*.cjs', + '**/*.mjs', + ], + rules: {}, + }, + format + ) ); return content; diff --git a/packages/eslint/src/generators/init/init-migration.ts b/packages/eslint/src/generators/init/init-migration.ts index 6e43b7a8613cf..354589ebc9239 100644 --- a/packages/eslint/src/generators/init/init-migration.ts +++ b/packages/eslint/src/generators/init/init-migration.ts @@ -32,10 +32,21 @@ export function migrateConfigToMonorepoStyle( projects: ProjectConfiguration[], tree: Tree, unitTestRunner: string, + eslintConfigFormat: 'mjs' | 'cjs', keepExistingVersions?: boolean ): GeneratorCallback { const rootEslintConfig = findEslintFile(tree); let skipCleanup = false; + + if (rootEslintConfig) { + // We do not want to mix the formats + eslintConfigFormat = tree + .read(rootEslintConfig, 'utf-8') + .includes('export default') + ? 'mjs' + : 'cjs'; + } + if ( rootEslintConfig?.match(/\.base\./) && !projects.some((p) => p.root === '.') @@ -57,10 +68,10 @@ export function migrateConfigToMonorepoStyle( keepExistingVersions ); tree.write( - tree.exists('eslint.config.cjs') - ? 'eslint.base.config.cjs' - : 'eslint.config.cjs', - getGlobalFlatEslintConfiguration() + tree.exists(`eslint.config.${eslintConfigFormat}`) + ? `eslint.base.config.${eslintConfigFormat}` + : `eslint.config.${eslintConfigFormat}`, + getGlobalFlatEslintConfiguration(eslintConfigFormat) ); } else { const eslintFile = findEslintFile(tree, '.'); @@ -134,7 +145,9 @@ function migrateEslintFile(projectEslintPath: string, tree: Tree) { let config = tree.read(projectEslintPath, 'utf-8'); // remove @nx plugin config = removePlugin(config, '@nx', '@nx/eslint-plugin-nx'); - // extend eslint.base.config.cjs + + // if base config is cjs, we will need to import it using async import + config = addImportToFlatConfig( config, 'baseConfig', diff --git a/packages/eslint/src/generators/init/init.spec.ts b/packages/eslint/src/generators/init/init.spec.ts index a43b6935e4235..1bc576716136b 100644 --- a/packages/eslint/src/generators/init/init.spec.ts +++ b/packages/eslint/src/generators/init/init.spec.ts @@ -102,43 +102,103 @@ describe('@nx/eslint:init', () => { }); describe('(legacy)', () => { - it('should add the root eslint config to the lint targetDefaults for lint', async () => { - await lintInitGenerator(tree, { ...options, addPlugin: false }); - - expect( - readJson(tree, 'nx.json').targetDefaults['@nx/eslint:lint'] - ).toEqual({ - cache: true, - inputs: [ - 'default', - '{workspaceRoot}/.eslintrc.json', - '{workspaceRoot}/.eslintignore', - '{workspaceRoot}/eslint.config.cjs', - ], + describe('CJS', () => { + it('should add the root eslint config to the lint targetDefaults for lint', async () => { + await lintInitGenerator(tree, { + ...options, + addPlugin: false, + eslintConfigFormat: 'cjs', + }); + + expect( + readJson(tree, 'nx.json').targetDefaults['@nx/eslint:lint'] + ).toEqual({ + cache: true, + inputs: [ + 'default', + '{workspaceRoot}/.eslintrc.json', + '{workspaceRoot}/.eslintignore', + '{workspaceRoot}/eslint.config.cjs', + ], + }); + }); + + it('should setup lint target defaults', async () => { + updateJson(tree, 'nx.json', (json) => { + json.namedInputs ??= {}; + json.namedInputs.production = ['default']; + return json; + }); + + await lintInitGenerator(tree, { + ...options, + addPlugin: false, + eslintConfigFormat: 'cjs', + }); + + expect( + readJson(tree, 'nx.json').targetDefaults[ + '@nx/eslint:lint' + ] + ).toEqual({ + cache: true, + inputs: [ + 'default', + '{workspaceRoot}/.eslintrc.json', + '{workspaceRoot}/.eslintignore', + '{workspaceRoot}/eslint.config.cjs', + ], + }); }); }); - it('should setup lint target defaults', async () => { - updateJson(tree, 'nx.json', (json) => { - json.namedInputs ??= {}; - json.namedInputs.production = ['default']; - return json; + describe('MJS', () => { + it('should add the root eslint config to the lint targetDefaults for lint', async () => { + await lintInitGenerator(tree, { + ...options, + addPlugin: false, + eslintConfigFormat: 'mjs', + }); + + expect( + readJson(tree, 'nx.json').targetDefaults['@nx/eslint:lint'] + ).toEqual({ + cache: true, + inputs: [ + 'default', + '{workspaceRoot}/.eslintrc.json', + '{workspaceRoot}/.eslintignore', + '{workspaceRoot}/eslint.config.mjs', + ], + }); }); - await lintInitGenerator(tree, { ...options, addPlugin: false }); - - expect( - readJson(tree, 'nx.json').targetDefaults[ - '@nx/eslint:lint' - ] - ).toEqual({ - cache: true, - inputs: [ - 'default', - '{workspaceRoot}/.eslintrc.json', - '{workspaceRoot}/.eslintignore', - '{workspaceRoot}/eslint.config.cjs', - ], + it('should setup lint target defaults', async () => { + updateJson(tree, 'nx.json', (json) => { + json.namedInputs ??= {}; + json.namedInputs.production = ['default']; + return json; + }); + + await lintInitGenerator(tree, { + ...options, + addPlugin: false, + eslintConfigFormat: 'mjs', + }); + + expect( + readJson(tree, 'nx.json').targetDefaults[ + '@nx/eslint:lint' + ] + ).toEqual({ + cache: true, + inputs: [ + 'default', + '{workspaceRoot}/.eslintrc.json', + '{workspaceRoot}/.eslintignore', + '{workspaceRoot}/eslint.config.mjs', + ], + }); }); }); }); diff --git a/packages/eslint/src/generators/init/init.ts b/packages/eslint/src/generators/init/init.ts index f297352d56d50..3fb1a741d9470 100644 --- a/packages/eslint/src/generators/init/init.ts +++ b/packages/eslint/src/generators/init/init.ts @@ -20,22 +20,24 @@ export interface LinterInitOptions { keepExistingVersions?: boolean; updatePackageScripts?: boolean; addPlugin?: boolean; + // Internal option + eslintConfigFormat?: 'mjs' | 'cjs'; } -function updateProductionFileset(tree: Tree) { +function updateProductionFileset(tree: Tree, format: 'mjs' | 'cjs' = 'mjs') { const nxJson = readNxJson(tree); const productionFileSet = nxJson.namedInputs?.production; if (productionFileSet) { productionFileSet.push('!{projectRoot}/.eslintrc.json'); - productionFileSet.push('!{projectRoot}/eslint.config.cjs'); + productionFileSet.push(`!{projectRoot}/eslint.config.${format}`); // Dedupe and set nxJson.namedInputs.production = Array.from(new Set(productionFileSet)); } updateNxJson(tree, nxJson); } -function addTargetDefaults(tree: Tree) { +function addTargetDefaults(tree: Tree, format: 'mjs' | 'cjs') { const nxJson = readNxJson(tree); nxJson.targetDefaults ??= {}; @@ -45,7 +47,7 @@ function addTargetDefaults(tree: Tree) { 'default', `{workspaceRoot}/.eslintrc.json`, `{workspaceRoot}/.eslintignore`, - `{workspaceRoot}/eslint.config.cjs`, + `{workspaceRoot}/eslint.config.${format}`, ]; updateNxJson(tree, nxJson); } @@ -74,9 +76,18 @@ export async function initEsLint( process.env.NX_ADD_PLUGINS !== 'false' && nxJson.useInferencePlugins !== false; options.addPlugin ??= addPluginDefault; + options.eslintConfigFormat ??= 'mjs'; const hasPlugin = hasEslintPlugin(tree); const rootEslintFile = findEslintFile(tree); + if (rootEslintFile) { + const rootEslintContent = tree.read(rootEslintFile, 'utf-8'); + // We do not want to mix the formats + options.eslintConfigFormat = rootEslintContent.includes('export default') + ? 'mjs' + : 'cjs'; + } + const graph = await createProjectGraphAsync(); const lintTargetNames = [ @@ -107,7 +118,7 @@ export async function initEsLint( return () => {}; } - updateProductionFileset(tree); + updateProductionFileset(tree, options.eslintConfigFormat); updateVsCodeRecommendedExtensions(tree); @@ -123,7 +134,7 @@ export async function initEsLint( options.updatePackageScripts ); } else { - addTargetDefaults(tree); + addTargetDefaults(tree, options.eslintConfigFormat); } const tasks: GeneratorCallback[] = []; diff --git a/packages/eslint/src/generators/lint-project/lint-project.spec.ts b/packages/eslint/src/generators/lint-project/lint-project.spec.ts index 2c194491af773..a7fa68a9020e0 100644 --- a/packages/eslint/src/generators/lint-project/lint-project.spec.ts +++ b/packages/eslint/src/generators/lint-project/lint-project.spec.ts @@ -42,7 +42,7 @@ describe('@nx/eslint:lint-project', () => { }); }); - it('should generate a flat eslint base config', async () => { + it('should generate a flat eslint base config ESM', async () => { const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG; process.env.ESLINT_USE_FLAT_CONFIG = 'true'; await lintProjectGenerator(tree, { @@ -51,6 +51,75 @@ describe('@nx/eslint:lint-project', () => { project: 'test-lib', setParserOptionsProject: false, skipFormat: true, + eslintConfigFormat: 'mjs', + }); + + expect(tree.read('eslint.config.mjs', 'utf-8')).toMatchInlineSnapshot(` + "import nx from "@nx/eslint-plugin"; + export default [ + ...nx.configs["flat/base"], + ...nx.configs["flat/typescript"], + ...nx.configs["flat/javascript"], + { + ignores: [ + "**/dist" + ] + }, + { + files: [ + "**/*.ts", + "**/*.tsx", + "**/*.js", + "**/*.jsx" + ], + rules: { + "@nx/enforce-module-boundaries": [ + "error", + { + enforceBuildableLibDependency: true, + allow: [ + "^.*/eslint(\\\\.base)?\\\\.config\\\\.[cm]?js$" + ], + depConstraints: [ + { + sourceTag: "*", + onlyDependOnLibsWithTags: [ + "*" + ] + } + ] + } + ] + } + }, + { + files: [ + "**/*.ts", + "**/*.tsx", + "**/*.js", + "**/*.jsx", + "**/*.cjs", + "**/*.mjs" + ], + // Override or add rules here + rules: {} + } + ]; + " + `); + process.env.ESLINT_USE_FLAT_CONFIG = originalEslintUseFlatConfigVal; + }); + + it('should generate a flat eslint base config CJS', async () => { + const originalEslintUseFlatConfigVal = process.env.ESLINT_USE_FLAT_CONFIG; + process.env.ESLINT_USE_FLAT_CONFIG = 'true'; + await lintProjectGenerator(tree, { + ...defaultOptions, + linter: Linter.EsLint, + project: 'test-lib', + setParserOptionsProject: false, + skipFormat: true, + eslintConfigFormat: 'cjs', }); expect(tree.read('eslint.config.cjs', 'utf-8')).toMatchInlineSnapshot(` diff --git a/packages/eslint/src/generators/lint-project/lint-project.ts b/packages/eslint/src/generators/lint-project/lint-project.ts index 13072d5342293..a4014c5e518b9 100644 --- a/packages/eslint/src/generators/lint-project/lint-project.ts +++ b/packages/eslint/src/generators/lint-project/lint-project.ts @@ -49,6 +49,7 @@ interface LintProjectOptions { rootProject?: boolean; keepExistingVersions?: boolean; addPlugin?: boolean; + eslintConfigFormat?: 'mjs' | 'cjs'; /** * @internal @@ -66,6 +67,7 @@ export async function lintProjectGeneratorInternal( options: LintProjectOptions ) { const nxJson = readNxJson(tree); + options.eslintConfigFormat ??= 'mjs'; const addPluginDefault = process.env.NX_ADD_PLUGINS !== 'false' && nxJson.useInferencePlugins !== false; @@ -74,12 +76,14 @@ export async function lintProjectGeneratorInternal( const initTask = await lintInitGenerator(tree, { skipPackageJson: options.skipPackageJson, addPlugin: options.addPlugin, + eslintConfigFormat: options.eslintConfigFormat, }); tasks.push(initTask); const rootEsLintTask = setupRootEsLint(tree, { unitTestRunner: options.unitTestRunner, skipPackageJson: options.skipPackageJson, rootProject: options.rootProject, + eslintConfigFormat: options.eslintConfigFormat, }); tasks.push(rootEsLintTask); const projectConfig = readProjectConfiguration(tree, options.project); @@ -146,6 +150,7 @@ export async function lintProjectGeneratorInternal( filteredProjects, tree, options.unitTestRunner, + options.eslintConfigFormat, options.keepExistingVersions ); tasks.push(migrateTask); @@ -199,6 +204,15 @@ function createEsLintConfiguration( const pathToRootConfig = extendedRootConfig ? `${offsetFromRoot(projectConfig.root)}${extendedRootConfig}` : undefined; + + if (extendedRootConfig) { + // We do not want to mix the formats + options.eslintConfigFormat = tree + .read(extendedRootConfig, 'utf-8') + .includes('export default') + ? 'mjs' + : 'cjs'; + } const addDependencyChecks = options.addPackageJsonDependencyChecks || isBuildableLibraryProject(projectConfig); @@ -269,11 +283,18 @@ function createEsLintConfiguration( nodes.push(generateSpreadElement('baseConfig')); } overrides.forEach((override) => { - nodes.push(generateFlatOverride(override)); + nodes.push(generateFlatOverride(override, options.eslintConfigFormat)); }); - const nodeList = createNodeList(importMap, nodes); + const nodeList = createNodeList( + importMap, + nodes, + options.eslintConfigFormat + ); const content = stringifyNodeList(nodeList); - tree.write(join(projectConfig.root, `eslint.config.mjs`), content); + tree.write( + join(projectConfig.root, `eslint.config.${options.eslintConfigFormat}`), + content + ); } else { writeJson(tree, join(projectConfig.root, `.eslintrc.json`), { extends: extendedRootConfig ? [pathToRootConfig] : undefined, diff --git a/packages/eslint/src/generators/lint-project/setup-root-eslint.ts b/packages/eslint/src/generators/lint-project/setup-root-eslint.ts index 3de9e0f99b397..ae93b0ae00fc4 100644 --- a/packages/eslint/src/generators/lint-project/setup-root-eslint.ts +++ b/packages/eslint/src/generators/lint-project/setup-root-eslint.ts @@ -22,6 +22,7 @@ export type SetupRootEsLintOptions = { unitTestRunner?: string; skipPackageJson?: boolean; rootProject?: boolean; + eslintConfigFormat?: 'mjs' | 'cjs'; }; export function setupRootEsLint( @@ -32,6 +33,8 @@ export function setupRootEsLint( if (rootEslintFile) { return () => {}; } + options.eslintConfigFormat ??= 'mjs'; + if (!useFlatConfig(tree)) { return setUpLegacyRootEslintRc(tree, options); } @@ -71,8 +74,11 @@ function setUpLegacyRootEslintRc(tree: Tree, options: SetupRootEsLintOptions) { function setUpRootFlatConfig(tree: Tree, options: SetupRootEsLintOptions) { tree.write( - 'eslint.config.cjs', - getGlobalFlatEslintConfiguration(options.rootProject) + `eslint.config.${options.eslintConfigFormat}`, + getGlobalFlatEslintConfiguration( + options.eslintConfigFormat, + options.rootProject + ) ); return !options.skipPackageJson diff --git a/packages/eslint/src/generators/utils/eslint-file.ts b/packages/eslint/src/generators/utils/eslint-file.ts index b85d43c79896e..f594c79347f07 100644 --- a/packages/eslint/src/generators/utils/eslint-file.ts +++ b/packages/eslint/src/generators/utils/eslint-file.ts @@ -75,7 +75,8 @@ export function isEslintConfigSupported(tree: Tree, projectRoot = ''): boolean { return ( eslintFile.endsWith('.json') || eslintFile.endsWith('.config.js') || - eslintFile.endsWith('.config.cjs') + eslintFile.endsWith('.config.cjs') || + eslintFile.endsWith('.config.mjs') ); } @@ -207,8 +208,10 @@ export function addOverrideToLintConfig( } } - const flatOverride = generateFlatOverride(override); let content = tree.read(fileName, 'utf8'); + const format = content.includes('export default') ? 'mjs' : 'cjs'; + + const flatOverride = generateFlatOverride(override, format); // Check if the provided override using legacy eslintrc properties or plugins, if so we need to add compat if (overrideNeedsCompat(override)) { content = addFlatCompatToFlatConfig(content); @@ -343,13 +346,14 @@ export function replaceOverridesInLintConfig( } } let content = tree.read(fileName, 'utf8'); + const format = content.includes('export default') ? 'mjs' : 'cjs'; // Check if any of the provided overrides using legacy eslintrc properties or plugins, if so we need to add compat if (overrides.some(overrideNeedsCompat)) { content = addFlatCompatToFlatConfig(content); } content = removeOverridesFromLintConfig(content); overrides.forEach((override) => { - const flatOverride = generateFlatOverride(override); + const flatOverride = generateFlatOverride(override, format); content = addBlockToFlatConfigExport(content, flatOverride); }); @@ -381,6 +385,14 @@ export function addExtendsToLintConfig( break; } } + // Check the file extension to determine the format of the config if it is .js we look for the export + const eslintConfigFormat = fileName.endsWith('.mjs') + ? 'mjs' + : fileName.endsWith('.cjs') + ? 'cjs' + : tree.read(fileName, 'utf-8').includes('module.exports') + ? 'cjs' + : 'mjs'; let shouldImportEslintCompat = false; // assume eslint version is 9 if not found, as it's what we'd be generating by default diff --git a/packages/eslint/src/generators/utils/flat-config/ast-utils.spec.ts b/packages/eslint/src/generators/utils/flat-config/ast-utils.spec.ts index b312d7112032e..e9b4d050220bd 100644 --- a/packages/eslint/src/generators/utils/flat-config/ast-utils.spec.ts +++ b/packages/eslint/src/generators/utils/flat-config/ast-utils.spec.ts @@ -30,7 +30,7 @@ describe('ast-utils', () => { it('should create appropriate ASTs for a flat config entries based on the provided legacy eslintrc JSON override data', () => { // It's easier to review the stringified result of the AST than the AST itself const getOutput = (input: any) => { - const ast = generateFlatOverride(input); + const ast = generateFlatOverride(input, 'mjs'); return printTsNode(ast); }; @@ -87,13 +87,13 @@ describe('ast-utils', () => { expect( getOutput({ - // It should not only nest the parser in languageOptions, but also wrap it in a require call because parsers are passed by reference in flat config + // It should not only nest the parser in languageOptions, but also wrap it in an import call because parsers are passed by reference in flat config parser: 'jsonc-eslint-parser', }) ).toMatchInlineSnapshot(` "{ languageOptions: { - parser: require("jsonc-eslint-parser") + parser: await import("jsonc-eslint-parser") } }" `); @@ -188,8 +188,8 @@ describe('ast-utils', () => { describe('addBlockToFlatConfigExport', () => { it('should inject block to the end of the file', () => { - const content = `const baseConfig = import("../../eslint.config.mjs"); - export default myExports = [ + const content = `import baseConfig from "../../eslint.config.mjs"; + export default [ ...baseConfig, { files: [ @@ -210,17 +210,17 @@ describe('ast-utils', () => { }) ); expect(result).toMatchInlineSnapshot(` - "const baseConfig = require("../../eslint.config.cjs"); - module.exports = [ - ...baseConfig, - { - files: [ - "my-lib/**/*.ts", - "my-lib/**/*.tsx" - ], - rules: {} - }, - { ignores: ["my-lib/.cache/**/*"] }, + "import baseConfig from "../../eslint.config.mjs"; + export default [ + ...baseConfig, + { + files: [ + "my-lib/**/*.ts", + "my-lib/**/*.tsx" + ], + rules: {} + }, + { ignores: ["my-lib/.cache/**/*"] }, { files: [ "**/*.svg" @@ -228,14 +228,15 @@ describe('ast-utils', () => { rules: { "@nx/do-something-with-svg": "error" } - }, - ];" + } + ]; + " `); }); it('should inject spread to the beginning of the file', () => { - const content = `const baseConfig = require("../../eslint.config.cjs"); - module.exports = [ + const content = `import baseConfig from "../../eslint.config.mjs"; + export default [ ...baseConfig, { files: [ @@ -252,28 +253,28 @@ describe('ast-utils', () => { { insertAtTheEnd: false } ); expect(result).toMatchInlineSnapshot(` - "const baseConfig = require("../../eslint.config.cjs"); - module.exports = [ + "import baseConfig from "../../eslint.config.mjs"; + export default [ ...config, - - ...baseConfig, - { - files: [ - "my-lib/**/*.ts", - "my-lib/**/*.tsx" - ], - rules: {} - }, - { ignores: ["my-lib/.cache/**/*"] }, - ];" + ...baseConfig, + { + files: [ + "my-lib/**/*.ts", + "my-lib/**/*.tsx" + ], + rules: {} + }, + { ignores: ["my-lib/.cache/**/*"] } + ]; + " `); }); }); describe('addImportToFlatConfig', () => { it('should inject import if not found', () => { - const content = `const baseConfig = require("../../eslint.config.cjs"); - module.exports = [ + const content = `import baseConfig from "../../eslint.config.mjs"; + export default [ ...baseConfig, { files: [ @@ -286,30 +287,30 @@ describe('ast-utils', () => { ];`; const result = addImportToFlatConfig( content, - 'varName', + ['varName'], '@myorg/awesome-config' ); expect(result).toMatchInlineSnapshot(` - "const varName = require("@myorg/awesome-config"); - const baseConfig = require("../../eslint.config.cjs"); - module.exports = [ - ...baseConfig, - { - files: [ - "my-lib/**/*.ts", - "my-lib/**/*.tsx" - ], - rules: {} - }, - { ignores: ["my-lib/.cache/**/*"] }, - ];" - `); + "import { varName } from "@myorg/awesome-config"; + import baseConfig from "../../eslint.config.mjs"; + export default [ + ...baseConfig, + { + files: [ + "my-lib/**/*.ts", + "my-lib/**/*.tsx" + ], + rules: {} + }, + { ignores: ["my-lib/.cache/**/*"] }, + ];" + `); }); it('should update import if already found', () => { - const content = `const { varName } = require("@myorg/awesome-config"); - const baseConfig = require("../../eslint.config.cjs"); - module.exports = [ + const content = `import { varName } from "@myorg/awesome-config"; + import baseConfig from "../../eslint.config.mjs"; + export default [ ...baseConfig, { files: [ @@ -326,26 +327,27 @@ describe('ast-utils', () => { '@myorg/awesome-config' ); expect(result).toMatchInlineSnapshot(` - "const { varName, otherName, someName } = require("@myorg/awesome-config"); - const baseConfig = require("../../eslint.config.cjs"); - module.exports = [ - ...baseConfig, - { - files: [ - "my-lib/**/*.ts", - "my-lib/**/*.tsx" - ], - rules: {} - }, - { ignores: ["my-lib/.cache/**/*"] }, - ];" - `); + "import { varName, otherName, someName } from "@myorg/awesome-config"; + import baseConfig from "../../eslint.config.mjs"; + export default [ + ...baseConfig, + { + files: [ + "my-lib/**/*.ts", + "my-lib/**/*.tsx" + ], + rules: {} + }, + { ignores: ["my-lib/.cache/**/*"] }, + ];" + `); }); it('should not inject import if already exists', () => { - const content = `const { varName, otherName } = require("@myorg/awesome-config"); - const baseConfig = require("../../eslint.config.cjs"); - module.exports = [ + const content = `import { varName, otherName } from "@myorg/awesome-config"; + import baseConfig from "../../eslint.config.mjs"; + + export default [ ...baseConfig, { files: [ @@ -365,9 +367,10 @@ describe('ast-utils', () => { }); it('should not update import if already exists', () => { - const content = `const varName = require("@myorg/awesome-config"); - const baseConfig = require("../../eslint.config.cjs"); - module.exports = [ + const content = `import { varName } from "@myorg/awesome-config"; + import baseConfig from "../../eslint.config.mjs"; + + export default [ ...baseConfig, { files: [ @@ -380,7 +383,7 @@ describe('ast-utils', () => { ];`; const result = addImportToFlatConfig( content, - 'varName', + ['varName'], '@myorg/awesome-config' ); expect(result).toEqual(content); @@ -390,10 +393,11 @@ describe('ast-utils', () => { describe('removeImportFromFlatConfig', () => { it('should remove existing import from config if the var name matches', () => { const content = stripIndents` - const nx = require("@nx/eslint-plugin"); - const thisShouldRemain = require("@nx/eslint-plugin"); - const playwright = require('eslint-plugin-playwright'); - module.exports = [ + import nx from "@nx/eslint-plugin"; + import thisShouldRemain from "@nx/eslint-plugin"; + import playwright from 'eslint-plugin-playwright'; + + export default [ playwright.configs['flat/recommended'], ]; `; @@ -404,9 +408,10 @@ describe('ast-utils', () => { ); expect(result).toMatchInlineSnapshot(` " - const thisShouldRemain = require("@nx/eslint-plugin"); - const playwright = require('eslint-plugin-playwright'); - module.exports = [ + import thisShouldRemain from "@nx/eslint-plugin"; + import playwright from 'eslint-plugin-playwright'; + + export default [ playwright.configs['flat/recommended'], ];" `); @@ -415,8 +420,8 @@ describe('ast-utils', () => { describe('addCompatToFlatConfig', () => { it('should add compat to config', () => { - const content = `const baseConfig = require("../../eslint.config.cjs"); - module.exports = [ + const content = `import baseConfig from "../../eslint.config.mjs"; + export default [ ...baseConfig, { files: [ @@ -429,15 +434,18 @@ describe('ast-utils', () => { ];`; const result = addFlatCompatToFlatConfig(content); expect(result).toMatchInlineSnapshot(` - "const { FlatCompat } = require("@eslint/eslintrc"); - const js = require("@eslint/js"); - const baseConfig = require("../../eslint.config.cjs"); + "import { FlatCompat } from "@eslint/eslintrc"; + import { dirname } from "path"; + import { fileURLToPath } from "url"; + import js from "@eslint/js"; + import baseConfig from "../../eslint.config.mjs"; const compat = new FlatCompat({ - baseDirectory: __dirname, + baseDirectory: dirname(fileURLToPath(import.meta.url)), recommendedConfig: js.configs.recommended, }); - module.exports = [ + + export default [ ...baseConfig, { files: [ @@ -452,9 +460,10 @@ describe('ast-utils', () => { }); it('should add only partially compat to config if parts exist', () => { - const content = `const baseConfig = require("../../eslint.config.cjs"); - const js = require("@eslint/js"); - module.exports = [ + const content = `import baseConfig from "../../eslint.config.mjs"; +import js from "@eslint/js"; + + export default [ ...baseConfig, { files: [ @@ -467,15 +476,19 @@ describe('ast-utils', () => { ];`; const result = addFlatCompatToFlatConfig(content); expect(result).toMatchInlineSnapshot(` - "const { FlatCompat } = require("@eslint/eslintrc"); - const baseConfig = require("../../eslint.config.cjs"); - const js = require("@eslint/js"); + "import { FlatCompat } from "@eslint/eslintrc"; + import { dirname } from "path"; + import { fileURLToPath } from "url"; + import baseConfig from "../../eslint.config.mjs"; + import js from "@eslint/js"; + const compat = new FlatCompat({ - baseDirectory: __dirname, + baseDirectory: dirname(fileURLToPath(import.meta.url)), recommendedConfig: js.configs.recommended, }); - module.exports = [ + + export default [ ...baseConfig, { files: [ @@ -490,16 +503,18 @@ describe('ast-utils', () => { }); it('should not add compat to config if exist', () => { - const content = `const FlatCompat = require("@eslint/eslintrc"); - const baseConfig = require("../../eslint.config.cjs"); - const js = require("@eslint/js"); + const content = `import { FlatCompat } from "@eslint/eslintrc"; + import baseConfig from "../../eslint.config.cjs"; + import js from "@eslint/js"; + import { fileURLToPath } from "url"; + import { dirname } from 'path'; const compat = new FlatCompat({ - baseDirectory: __dirname, + baseDirectory: dirname(fileURLToPath(import.meta.url)), recommendedConfig: js.configs.recommended, }); - module.exports = [ + export default [ ...baseConfig, { files: [ @@ -517,16 +532,17 @@ describe('ast-utils', () => { describe('removeOverridesFromLintConfig', () => { it('should remove all rules from config', () => { - const content = `const FlatCompat = require("@eslint/eslintrc"); - const baseConfig = require("../../eslint.config.cjs"); - const js = require("@eslint/js"); + const content = `import { FlatCompat } from "@eslint/eslintrc"; + import js from "@eslint/js"; + import { fileURLToPath } from "url"; + import { dirname } from 'path'; const compat = new FlatCompat({ - baseDirectory: __dirname, + baseDirectory: dirname(fileURLToPath(import.meta.url)), recommendedConfig: js.configs.recommended, }); - module.exports = [ + export default [ ...baseConfig, { files: [ @@ -557,26 +573,27 @@ describe('ast-utils', () => { ];`; const result = removeOverridesFromLintConfig(content); expect(result).toMatchInlineSnapshot(` - "const FlatCompat = require("@eslint/eslintrc"); - const baseConfig = require("../../eslint.config.cjs"); - const js = require("@eslint/js"); - - const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, - }); - - module.exports = [ - ...baseConfig, - { ignores: ["my-lib/.cache/**/*"] }, - ];" - `); + "import { FlatCompat } from "@eslint/eslintrc"; + import js from "@eslint/js"; + import { fileURLToPath } from "url"; + import { dirname } from 'path'; + + const compat = new FlatCompat({ + baseDirectory: dirname(fileURLToPath(import.meta.url)), + recommendedConfig: js.configs.recommended, + }); + + export default [ + ...baseConfig, + { ignores: ["my-lib/.cache/**/*"] }, + ];" + `); }); it('should remove all rules from starting with first', () => { - const content = `const baseConfig = require("../../eslint.config.cjs"); + const content = `import baseConfig from "../../eslint.config.mjs"; - module.exports = [ + export default [ { files: [ "my-lib/**/*.ts", @@ -605,19 +622,19 @@ describe('ast-utils', () => { ];`; const result = removeOverridesFromLintConfig(content); expect(result).toMatchInlineSnapshot(` - "const baseConfig = require("../../eslint.config.cjs"); + "import baseConfig from "../../eslint.config.mjs"; - module.exports = [ - ];" - `); + export default [ + ];" + `); }); }); describe('replaceOverride', () => { it('should find and replace rules in override', () => { - const content = `const baseConfig = require("../../eslint.config.cjs"); + const content = `import baseConfig from "../../eslint.config.mjs"; -module.exports = [ +export default [ { files: [ "my-lib/**/*.ts", @@ -657,9 +674,9 @@ module.exports = [ }) ); expect(result).toMatchInlineSnapshot(` - "const baseConfig = require("../../eslint.config.cjs"); + "import baseConfig from "../../eslint.config.mjs"; - module.exports = [ + export default [ { "files": [ "my-lib/**/*.ts", @@ -692,9 +709,9 @@ module.exports = [ }); it('should append rules in override', () => { - const content = `const baseConfig = require("../../eslint.config.cjs"); + const content = `import baseConfig from "../../eslint.config.mjs"; -module.exports = [ +export default [ { files: [ "my-lib/**/*.ts", @@ -728,9 +745,9 @@ module.exports = [ }) ); expect(result).toMatchInlineSnapshot(` - "const baseConfig = require("../../eslint.config.cjs"); + "import baseConfig from "../../eslint.config.mjs"; - module.exports = [ + export default [ { "files": [ "my-lib/**/*.ts", @@ -755,9 +772,9 @@ module.exports = [ }); it('should work for compat overrides', () => { - const content = `const baseConfig = require("../../eslint.config.cjs"); + const content = `import baseConfig from "../../eslint.config.mjs"; -module.exports = [ +export default [ ...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({ ...config, files: [ @@ -783,9 +800,9 @@ module.exports = [ }) ); expect(result).toMatchInlineSnapshot(` - "const baseConfig = require("../../eslint.config.cjs"); + "import baseConfig from "../../eslint.config.mjs"; - module.exports = [ + export default [ ...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({ ...config, "files": [ @@ -804,14 +821,17 @@ module.exports = [ 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 content = `import { FlatCompat } from "@eslint/eslintrc"; + import nxEslintPlugin from "@nx/eslint-plugin"; + import js = from ("@eslint/js"); + import { fileURLToPath} from "url"; + import { dirname } from 'path'; + const compat = new FlatCompat({ - baseDirectory: __dirname, + baseDirectory: dirname(fileURLToPath(import.meta.url)); recommendedConfig: js.configs.recommended, }); - module.exports = [ + export default [ { plugins: { "@nx": nxEslintPlugin } }, { ignores: ["src/ignore/to/keep.ts"] }, { ignores: ["something/else"] } @@ -819,13 +839,16 @@ module.exports = [ const result = removePlugin(content, '@nx', '@nx/eslint-plugin'); expect(result).toMatchInlineSnapshot(` - "const { FlatCompat } = require("@eslint/eslintrc"); - const js = require("@eslint/js"); + "import { FlatCompat } from "@eslint/eslintrc"; + import js = from ("@eslint/js"); + import { fileURLToPath} from "url"; + import { dirname } from 'path'; + const compat = new FlatCompat({ - baseDirectory: __dirname, + baseDirectory: dirname(fileURLToPath(import.meta.url)); recommendedConfig: js.configs.recommended, }); - module.exports = [ + export default [ { ignores: ["src/ignore/to/keep.ts"] }, { ignores: ["something/else"] } ];" @@ -833,15 +856,19 @@ module.exports = [ }); 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 content = `import { FlatCompat } from "@eslint/eslintrc"; + import nxEslintPlugin from "@nx/eslint-plugin"; + import otherPlugin from "other/eslint-plugin"; + import js from "@eslint/js"; + import { fileURLToPath } from "url"; + import { dirname } from 'path'; + const compat = new FlatCompat({ - baseDirectory: __dirname, + baseDirectory: dirname(fileURLToPath(import.meta.url)), recommendedConfig: js.configs.recommended, }); - module.exports = [ + + export default [ { plugins: { "@nx": nxEslintPlugin, "@other": otherPlugin } }, { ignores: ["src/ignore/to/keep.ts"] }, { ignores: ["something/else"] } @@ -849,14 +876,18 @@ module.exports = [ 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"); + "import { FlatCompat } from "@eslint/eslintrc"; + import otherPlugin from "other/eslint-plugin"; + import js from "@eslint/js"; + import { fileURLToPath } from "url"; + import { dirname } from 'path'; + const compat = new FlatCompat({ - baseDirectory: __dirname, + baseDirectory: dirname(fileURLToPath(import.meta.url)), recommendedConfig: js.configs.recommended, }); - module.exports = [ + + export default [ { plugins: { "@other": otherPlugin } }, { ignores: ["src/ignore/to/keep.ts"] }, { ignores: ["something/else"] } @@ -865,14 +896,18 @@ module.exports = [ }); 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 content = `import { FlatCompat } from "@eslint/eslintrc"; + import nxEslintPlugin from "@nx/eslint-plugin"; + import js from "@eslint/js"; + import { fileURLToPath } from "url"; + import { dirname } from 'path'; + const compat = new FlatCompat({ - baseDirectory: __dirname, + baseDirectory: dirname(fileURLToPath(import.meta.url)), recommendedConfig: js.configs.recommended, }); - module.exports = [ + + export default [ { plugins: { "@nx": nxEslintPlugin }, rules: {} }, { ignores: ["src/ignore/to/keep.ts"] }, { ignores: ["something/else"] } @@ -880,13 +915,17 @@ module.exports = [ const result = removePlugin(content, '@nx', '@nx/eslint-plugin'); expect(result).toMatchInlineSnapshot(` - "const { FlatCompat } = require("@eslint/eslintrc"); - const js = require("@eslint/js"); + "import { FlatCompat } from "@eslint/eslintrc"; + import js from "@eslint/js"; + import { fileURLToPath } from "url"; + import { dirname } from 'path'; + const compat = new FlatCompat({ - baseDirectory: __dirname, + baseDirectory: dirname(fileURLToPath(import.meta.url)), recommendedConfig: js.configs.recommended, }); - module.exports = [ + + export default [ { rules: {} }, { ignores: ["src/ignore/to/keep.ts"] }, { ignores: ["something/else"] } @@ -895,14 +934,18 @@ module.exports = [ }); 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 content = `import { FlatCompat } from "@eslint/eslintrc"; + import nxEslintPlugin from "@nx/eslint-plugin"; + import js from "@eslint/js"; + import { fileURLToPath } from "url"; + import { dirname } from 'path'; + const compat = new FlatCompat({ - baseDirectory: __dirname, + baseDirectory: dirname(fileURLToPath(import.meta.url)), recommendedConfig: js.configs.recommended, }); - module.exports = [ + + export default [ { plugins: ["@nx", "something-else"] }, { ignores: ["src/ignore/to/keep.ts"] }, { ignores: ["something/else"] } @@ -910,13 +953,17 @@ module.exports = [ const result = removePlugin(content, '@nx', '@nx/eslint-plugin'); expect(result).toMatchInlineSnapshot(` - "const { FlatCompat } = require("@eslint/eslintrc"); - const js = require("@eslint/js"); + "import { FlatCompat } from "@eslint/eslintrc"; + import js from "@eslint/js"; + import { fileURLToPath } from "url"; + import { dirname } from 'path'; + const compat = new FlatCompat({ - baseDirectory: __dirname, + baseDirectory: dirname(fileURLToPath(import.meta.url)), recommendedConfig: js.configs.recommended, }); - module.exports = [ + + export default [ { plugins:["something-else"] }, { ignores: ["src/ignore/to/keep.ts"] }, { ignores: ["something/else"] } @@ -925,14 +972,18 @@ module.exports = [ }); 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 content = `import { FlatCompat } from "@eslint/eslintrc"; + import nxEslintPlugin from "@nx/eslint-plugin"; + import js from "@eslint/js"; + import { fileURLToPath } from "url"; + import { dirname } from 'path'; + const compat = new FlatCompat({ - baseDirectory: __dirname, + baseDirectory: dirname(fileURLToPath(import.meta.url)), recommendedConfig: js.configs.recommended, }); - module.exports = [ + + export default [ { plugins: ["@nx"], rules: { } }, { ignores: ["src/ignore/to/keep.ts"] }, { ignores: ["something/else"] } @@ -940,13 +991,17 @@ module.exports = [ const result = removePlugin(content, '@nx', '@nx/eslint-plugin'); expect(result).toMatchInlineSnapshot(` - "const { FlatCompat } = require("@eslint/eslintrc"); - const js = require("@eslint/js"); + "import { FlatCompat } from "@eslint/eslintrc"; + import js from "@eslint/js"; + import { fileURLToPath } from "url"; + import { dirname } from 'path'; + const compat = new FlatCompat({ - baseDirectory: __dirname, + baseDirectory: dirname(fileURLToPath(import.meta.url)), recommendedConfig: js.configs.recommended, }); - module.exports = [ + + export default [ { rules: { } }, { ignores: ["src/ignore/to/keep.ts"] }, { ignores: ["something/else"] } @@ -955,14 +1010,19 @@ module.exports = [ }); 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 content = `import { FlatCompat } from "@eslint/eslintrc"; + import nxEslintPlugin from "@nx/eslint-plugin"; + import js from "@eslint/js"; + + import { fileURLToPath } from "url"; + import { dirname } from 'path'; + const compat = new FlatCompat({ - baseDirectory: __dirname, + baseDirectory: dirname(fileURLToPath(import.meta.url)), recommendedConfig: js.configs.recommended, }); - module.exports = [ + + export default [ { plugins: ["@nx"] }, { ignores: ["src/ignore/to/keep.ts"] }, { ignores: ["something/else"] } @@ -970,13 +1030,18 @@ module.exports = [ const result = removePlugin(content, '@nx', '@nx/eslint-plugin'); expect(result).toMatchInlineSnapshot(` - "const { FlatCompat } = require("@eslint/eslintrc"); - const js = require("@eslint/js"); + "import { FlatCompat } from "@eslint/eslintrc"; + import js from "@eslint/js"; + + import { fileURLToPath } from "url"; + import { dirname } from 'path'; + const compat = new FlatCompat({ - baseDirectory: __dirname, + baseDirectory: dirname(fileURLToPath(import.meta.url)), recommendedConfig: js.configs.recommended, }); - module.exports = [ + + export default [ { ignores: ["src/ignore/to/keep.ts"] }, { ignores: ["something/else"] } ];" @@ -986,14 +1051,18 @@ module.exports = [ 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 content = `import { FlatCompat } from "@eslint/eslintrc"; + import nxEslintPlugin from "@nx/eslint-plugin"; + import js from "@eslint/js"; + import { fileURLToPath } from "url"; + import { dirname } from "path"; + const compat = new FlatCompat({ - baseDirectory: __dirname, + baseDirectory: dirname(fileURLToPath(import.meta.url)), recommendedConfig: js.configs.recommended, }); - module.exports = [ + + export default [ { plugins: { "@nx": nxEslintPlugin } }, ...compat.config({ extends: ["plugin:@nx/typescript"] }).map(config => ({ ...config, @@ -1013,14 +1082,18 @@ module.exports = [ 'plugin:@nx/javascript', ]); expect(result).toMatchInlineSnapshot(` - "const { FlatCompat } = require("@eslint/eslintrc"); - const nxEslintPlugin = require("@nx/eslint-plugin"); - const js = require("@eslint/js"); + "import { FlatCompat } from "@eslint/eslintrc"; + import nxEslintPlugin from "@nx/eslint-plugin"; + import js from "@eslint/js"; + import { fileURLToPath } from "url"; + import { dirname } from "path"; + const compat = new FlatCompat({ - baseDirectory: __dirname, + baseDirectory: dirname(fileURLToPath(import.meta.url)), recommendedConfig: js.configs.recommended, }); - module.exports = [ + + export default [ { plugins: { "@nx": nxEslintPlugin } }, { files: ['*.ts', '*.tsx', '*.js', '*.jsx'], @@ -1039,9 +1112,10 @@ module.exports = [ describe('removePredefinedConfigs', () => { it('should remove config objects and import', () => { const content = stripIndents` - const nx = require("@nx/eslint-plugin"); - const playwright = require('eslint-plugin-playwright'); - module.exports = [ + import nx from "@nx/eslint-plugin"; + import playwright from 'eslint-plugin-playwright'; + + export default [ ...nx.config['flat/base'], ...nx.config['flat/typescript'], ...nx.config['flat/javascript'], @@ -1058,8 +1132,9 @@ module.exports = [ expect(result).toMatchInlineSnapshot(` " - const playwright = require('eslint-plugin-playwright'); - module.exports = [ + import playwright from 'eslint-plugin-playwright'; + + export default [ playwright.configs['flat/recommended'], ];" `); @@ -1067,9 +1142,10 @@ module.exports = [ it('should keep configs that are not in the list', () => { const content = stripIndents` - const nx = require("@nx/eslint-plugin"); - const playwright = require('eslint-plugin-playwright'); - module.exports = [ + import nx from "@nx/eslint-plugin"; + import playwright from 'eslint-plugin-playwright'; + + export default [ ...nx.config['flat/base'], ...nx.config['flat/typescript'], ...nx.config['flat/javascript'], @@ -1086,9 +1162,10 @@ module.exports = [ ); expect(result).toMatchInlineSnapshot(` - "const nx = require("@nx/eslint-plugin"); - const playwright = require('eslint-plugin-playwright'); - module.exports = [ + "import nx from "@nx/eslint-plugin"; + import playwright from 'eslint-plugin-playwright'; + + export default [ ...nx.config['flat/react'], playwright.configs['flat/recommended'], ];" diff --git a/packages/eslint/src/generators/utils/flat-config/ast-utils.ts b/packages/eslint/src/generators/utils/flat-config/ast-utils.ts index e1502908af654..d50c030c374a8 100644 --- a/packages/eslint/src/generators/utils/flat-config/ast-utils.ts +++ b/packages/eslint/src/generators/utils/flat-config/ast-utils.ts @@ -26,7 +26,10 @@ export function removeOverridesFromLintConfig(content: string): string { ts.ScriptKind.JS ); - const exportsArray = findAllBlocks(source); + const format = content.includes('export default') ? 'mjs' : 'cjs'; + + const exportsArray = + format === 'mjs' ? findAllBlocks(source) : findModuleExports(source); if (!exportsArray) { return content; } @@ -47,7 +50,19 @@ export function removeOverridesFromLintConfig(content: string): string { return applyChangesToString(content, changes); } +// TODO Change name function findAllBlocks(source: ts.SourceFile): ts.NodeArray { + return ts.forEachChild(source, function analyze(node) { + if ( + ts.isExportAssignment(node) && + ts.isArrayLiteralExpression(node.expression) + ) { + return node.expression.elements; + } + }); +} + +function findModuleExports(source: ts.SourceFile): ts.NodeArray { return ts.forEachChild(source, function analyze(node) { if ( ts.isExpressionStatement(node) && @@ -86,7 +101,9 @@ export function hasOverride( true, ts.ScriptKind.JS ); - const exportsArray = findAllBlocks(source); + const format = content.includes('export default') ? 'mjs' : 'cjs'; + const exportsArray = + format === 'mjs' ? findAllBlocks(source) : findModuleExports(source); if (!exportsArray) { return false; } @@ -120,6 +137,7 @@ function parseTextToJson(text: string): any { .replace(/\s([a-zA-Z0-9_]+)\s*:/g, ' "$1": ') // stringify any require calls to avoid JSON parsing errors, turn them into just the string value being required .replace(/require\(['"]([^'"]+)['"]\)/g, '"$1"') + .replace(/\(?await import\(['"]([^'"]+)['"]\)\)?/g, '"$1"') ); } @@ -141,7 +159,9 @@ export function replaceOverride( true, ts.ScriptKind.JS ); - const exportsArray = findAllBlocks(source); + const format = content.includes('export default') ? 'mjs' : 'cjs'; + const exportsArray = + format === 'mjs' ? findAllBlocks(source) : findModuleExports(source); if (!exportsArray) { return content; } @@ -174,20 +194,24 @@ export function replaceOverride( let updatedData = update(data); if (updatedData) { updatedData = mapFilePaths(updatedData); + + const parserReplacement = + format === 'mjs' + ? (parser: string) => `(await import('${parser}'))` + : (parser: string) => `require('${parser}')`; + changes.push({ type: ChangeType.Insert, index: start, - // NOTE: Indentation added to format without formatting tools like Prettier. text: ' ' + JSON.stringify(updatedData, null, 2) - // restore any parser require calls that were stripped during JSON parsing - .replace(/"parser": "([^"]+)"/g, (_, parser) => { - return `"parser": require('${parser}')`; - }) - .slice(2, -2) // remove curly braces and start/end line breaks since we are injecting just properties - // Append indentation so file is formatted without Prettier - .replaceAll(/\n/g, '\n '), + .replace( + /"parser": "([^"]+)"/g, + (_, parser) => `"parser": ${parserReplacement(parser)}` + ) + .slice(2, -2) // Remove curly braces and start/end line breaks + .replaceAll(/\n/g, '\n '), // Maintain indentation }); } } @@ -198,7 +222,12 @@ export function replaceOverride( } /** - * Adding require statement to the top of the file + * Adding import statement to the top of the file + * The imports are added based on a few rules: + * 1. If it's a default import and matches the variable, return content unchanged. + * 2. If it's a named import and the variables are not part of the import object, add them. + * 3. If no existing import and variable is a string, add a default import. + * 4. If no existing import and variable is an array, add it as an object import. */ export function addImportToFlatConfig( content: string, @@ -214,55 +243,208 @@ export function addImportToFlatConfig( ts.ScriptKind.JS ); - const foundBindingVars: ts.ImportSpecifier[] = []; - ts.forEachChild(source, function analyze(node) { - // we can only combine object binding patterns - if (!Array.isArray(variable)) { - return; - } + const format = content.includes('export default') ? 'mjs' : 'cjs'; + + if (format === 'mjs') { + return addESMImportToFlatConfig(source, printer, content, variable, imp); + } + return addCJSImportToFlatConfig(source, printer, content, variable, imp); +} + +function addESMImportToFlatConfig( + source: ts.SourceFile, + printer: ts.Printer, + content: string, + variable: string | string[], + imp: string +): string { + let existingImport: ts.ImportDeclaration | undefined; + + ts.forEachChild(source, (node) => { if ( ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && - node.moduleSpecifier.text === imp && - node.importClause && - ts.isNamedImports(node.importClause.namedBindings) + node.moduleSpecifier.text === imp ) { - foundBindingVars.push(...node.importClause.namedBindings.elements); + existingImport = node; } }); - if (foundBindingVars.length > 0 && Array.isArray(variable)) { + // Rule 1: + if ( + existingImport && + typeof variable === 'string' && + existingImport.importClause?.name?.getText() === variable + ) { + return content; + } + + // Rule 2: + if ( + existingImport && + existingImport.importClause?.namedBindings && + Array.isArray(variable) + ) { + const namedImports = existingImport.importClause + .namedBindings as ts.NamedImports; + const existingElements = namedImports.elements; + + // Filter out variables that are already imported const newVariables = variable.filter( - (v) => !foundBindingVars.some((fv) => v === fv.name.getText()) + (v) => !existingElements.some((e) => e.name.getText() === v) ); + if (newVariables.length === 0) { return content; } - const lastElement = foundBindingVars[foundBindingVars.length - 1]; - const pos = lastElement ? lastElement.getEnd() : source.getEnd(); + const newImportSpecifiers = newVariables.map((v) => + ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier(v) + ) + ); + + const lastElement = existingElements[existingElements.length - 1]; + const insertIndex = lastElement + ? lastElement.getEnd() + : namedImports.getEnd(); + + const insertText = printer.printList( + ts.ListFormat.NamedImportsOrExportsElements, + ts.factory.createNodeArray(newImportSpecifiers), + source + ); + + return applyChangesToString(content, [ + { + type: ChangeType.Insert, + index: insertIndex, + text: `, ${insertText}`, + }, + ]); + } + + // Rule 3: + if (!existingImport && typeof variable === 'string') { + const defaultImport = ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause( + false, + ts.factory.createIdentifier(variable), + undefined + ), + ts.factory.createStringLiteral(imp) + ); + + const insert = printer.printNode( + ts.EmitHint.Unspecified, + defaultImport, + source + ); + + return applyChangesToString(content, [ + { + type: ChangeType.Insert, + index: 0, + text: `${insert}\n`, + }, + ]); + } + + // Rule 4: + if (!existingImport && Array.isArray(variable)) { + const objectImport = ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause( + false, + undefined, + ts.factory.createNamedImports( + variable.map((v) => + ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier(v) + ) + ) + ) + ), + ts.factory.createStringLiteral(imp) + ); + + const insert = printer.printNode( + ts.EmitHint.Unspecified, + objectImport, + source + ); + + return applyChangesToString(content, [ + { + type: ChangeType.Insert, + index: 0, + text: `${insert}\n`, + }, + ]); + } +} +function addCJSImportToFlatConfig( + source: ts.SourceFile, + printer: ts.Printer, + content: string, + variable: string | string[], + imp: string +): string { + const foundBindingVars: ts.NodeArray = ts.forEachChild( + source, + function analyze(node) { + // we can only combine object binding patterns + if (!Array.isArray(variable)) { + return; + } + if ( + ts.isVariableStatement(node) && + ts.isVariableDeclaration(node.declarationList.declarations[0]) && + ts.isObjectBindingPattern(node.declarationList.declarations[0].name) && + ts.isCallExpression(node.declarationList.declarations[0].initializer) && + node.declarationList.declarations[0].initializer.expression.getText() === + 'require' && + ts.isStringLiteral( + node.declarationList.declarations[0].initializer.arguments[0] + ) && + node.declarationList.declarations[0].initializer.arguments[0].text === + imp + ) { + return node.declarationList.declarations[0].name.elements; + } + } + ); + + if (foundBindingVars && Array.isArray(variable)) { + const newVariables = variable.filter( + (v) => !foundBindingVars.some((fv) => v === fv.name.getText()) + ); + if (newVariables.length === 0) { + return content; + } + const isMultiLine = foundBindingVars.hasTrailingComma; + const pos = foundBindingVars.end; const nodes = ts.factory.createNodeArray( newVariables.map((v) => - ts.factory.createImportSpecifier( - false, - undefined, - ts.factory.createIdentifier(v) - ) + ts.factory.createBindingElement(undefined, undefined, v) ) ); - const insert = printer.printList( - ts.ListFormat.NamedImportsOrExportsElements, + ts.ListFormat.ObjectBindingPatternElements, nodes, source ); - return applyChangesToString(content, [ { type: ChangeType.Insert, index: pos, - text: `, ${insert}`, + text: isMultiLine ? `,\n${insert}` : `,${insert}`, }, ]); } @@ -275,11 +457,18 @@ export function addImportToFlatConfig( return; } if ( - ts.isImportDeclaration(node) && - ts.isStringLiteral(node.moduleSpecifier) && - node.moduleSpecifier.text === imp && - ts.isImportClause(node.importClause) && - node.importClause.name?.getText() === variable + 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; } @@ -291,7 +480,7 @@ export function addImportToFlatConfig( } // the import was not found, create a new one - const requireStatement = generateImport( + const requireStatement = generateRequire( typeof variable === 'string' ? variable : ts.factory.createObjectBindingPattern( @@ -315,6 +504,22 @@ export function addImportToFlatConfig( ]); } +function existsAsNamedOrDefaultImport( + node: ts.ImportDeclaration, + variable: string | string[] +) { + const isNamed = + node.importClause.namedBindings && + ts.isNamedImports(node.importClause.namedBindings); + if (Array.isArray(variable)) { + return isNamed || variable.includes(node.importClause?.name?.getText()); + } + return ( + (node.importClause.namedBindings && + ts.isNamedImports(node.importClause.namedBindings)) || + node.importClause?.name?.getText() === variable + ); +} /** * Remove an import from flat config */ @@ -331,8 +536,49 @@ export function removeImportFromFlatConfig( ts.ScriptKind.JS ); + const format = content.includes('export default') ? 'mjs' : 'cjs'; + if (format === 'mjs') { + return removeImportFromFlatConfigESM(source, content, variable, imp); + } else { + return removeImportFromFlatConfigCJS(source, content, variable, imp); + } +} + +function removeImportFromFlatConfigESM( + source: ts.SourceFile, + content: string, + variable: string, + imp: string +): string { const changes: StringChange[] = []; + ts.forEachChild(source, (node) => { + // we can only combine object binding patterns + if ( + ts.isImportDeclaration(node) && + ts.isStringLiteral(node.moduleSpecifier) && + node.moduleSpecifier.text === imp && + node.importClause && + existsAsNamedOrDefaultImport(node, variable) + ) { + changes.push({ + type: ChangeType.Delete, + start: node.pos, + length: node.end - node.pos, + }); + } + }); + + return applyChangesToString(content, changes); +} + +function removeImportFromFlatConfigCJS( + source: ts.SourceFile, + content: string, + variable: string, + imp: string +): string { + const changes: StringChange[] = []; ts.forEachChild(source, (node) => { // we can only combine object binding patterns if ( @@ -360,7 +606,7 @@ export function removeImportFromFlatConfig( } /** - * Injects new ts.expression to the end of the module.exports array. + * Injects new ts.expression to the end of the module.exports or export default array. */ export function addBlockToFlatConfigExport( content: string, @@ -378,12 +624,85 @@ export function addBlockToFlatConfigExport( ts.ScriptKind.JS ); + const format = content.includes('export default') ? 'mjs' : 'cjs'; + + // find the export default array statement + if (format === 'mjs') { + return addBlockToFlatConfigExportESM( + content, + config, + source, + printer, + options + ); + } else { + return addBlockToFlatConfigExportCJS( + content, + config, + source, + printer, + options + ); + } +} + +function addBlockToFlatConfigExportESM( + content: string, + config: ts.Expression | ts.SpreadElement, + source: ts.SourceFile, + printer: ts.Printer, + options: { insertAtTheEnd?: boolean; checkBaseConfig?: boolean } = { + insertAtTheEnd: true, + } +): string { + const exportDefaultStatement = source.statements.find( + (statement): statement is ts.ExportAssignment => + ts.isExportAssignment(statement) && + ts.isArrayLiteralExpression(statement.expression) + ); + + if (!exportDefaultStatement) return content; + + const exportArrayLiteral = + exportDefaultStatement.expression as ts.ArrayLiteralExpression; + + const updatedArrayElements = options.insertAtTheEnd + ? [...exportArrayLiteral.elements, config] + : [config, ...exportArrayLiteral.elements]; + + const updatedExportDefault = ts.factory.createExportAssignment( + undefined, + false, + ts.factory.createArrayLiteralExpression(updatedArrayElements, true) + ); + + // update the existing export default array + const updatedStatements = source.statements.map((statement) => + statement === exportDefaultStatement ? updatedExportDefault : statement + ); + + const updatedSource = ts.factory.updateSourceFile(source, updatedStatements); + + return printer.printFile(updatedSource); +} + +function addBlockToFlatConfigExportCJS( + content: string, + config: ts.Expression | ts.SpreadElement, + source: ts.SourceFile, + printer: ts.Printer, + options: { insertAtTheEnd?: boolean; checkBaseConfig?: boolean } = { + insertAtTheEnd: true, + } +): string { const exportsArray = ts.forEachChild(source, function analyze(node) { if ( - ts.isExportAssignment(node) && - ts.isArrayLiteralExpression(node.expression) + ts.isExpressionStatement(node) && + ts.isBinaryExpression(node.expression) && + node.expression.left.getText() === 'module.exports' && + ts.isArrayLiteralExpression(node.expression.right) ) { - return node.expression.elements; + return node.expression.right.elements; } }); @@ -392,17 +711,34 @@ export function addBlockToFlatConfigExport( // base config was not generated by Nx. if (!exportsArray) return content; - const newArray = options.insertAtTheEnd - ? [...exportsArray, config] - : [config, ...exportsArray]; - - const updatedExport = ts.factory.createExportAssignment( - undefined, - false, - ts.factory.createArrayLiteralExpression(newArray, true) - ); - - return printer.printNode(ts.EmitHint.Unspecified, updatedExport, source); + const insert = + ' ' + + printer + .printNode(ts.EmitHint.Expression, config, source) + .replaceAll(/\n/g, '\n '); + if (options.insertAtTheEnd) { + const index = + exportsArray.length > 0 + ? exportsArray.at(exportsArray.length - 1).end + : exportsArray.pos; + return applyChangesToString(content, [ + { + type: ChangeType.Insert, + index, + text: `,\n${insert}`, + }, + ]); + } else { + const index = + exportsArray.length > 0 ? exportsArray.at(0).pos : exportsArray.pos; + return applyChangesToString(content, [ + { + type: ChangeType.Insert, + index, + text: `\n${insert},\n`, + }, + ]); + } } export function removePlugin( @@ -417,34 +753,57 @@ export function removePlugin( true, ts.ScriptKind.JS ); + const format = content.includes('export default') ? 'mjs' : 'cjs'; const changes: StringChange[] = []; + if (format === 'mjs') { + ts.forEachChild(source, function analyze(node) { + if ( + ts.isImportDeclaration(node) && + ts.isStringLiteral(node.moduleSpecifier) && + node.moduleSpecifier.text === pluginImport + ) { + const importClause = node.importClause; + + if ( + (importClause && importClause.name) || + (importClause.namedBindings && + ts.isNamedImports(importClause.namedBindings)) + ) { + changes.push({ + type: ChangeType.Delete, + start: node.pos, + length: node.end - node.pos, + }); + } + } + }); + } else { + ts.forEachChild(source, function analyze(node) { + if ( + ts.isVariableStatement(node) && + ts.isVariableDeclaration(node.declarationList.declarations[0]) && + ts.isCallExpression(node.declarationList.declarations[0].initializer) && + node.declarationList.declarations[0].initializer.arguments.length && + ts.isStringLiteral( + node.declarationList.declarations[0].initializer.arguments[0] + ) && + node.declarationList.declarations[0].initializer.arguments[0].text === + pluginImport + ) { + changes.push({ + type: ChangeType.Delete, + start: node.pos, + length: node.end - node.pos, + }); + } + }); + } 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) + ts.isExportAssignment(node) && + ts.isArrayLiteralExpression(node.expression) ) { - const blockElements = node.expression.right.elements; + const blockElements = node.expression.elements; blockElements.forEach((element) => { if (ts.isObjectLiteralExpression(element)) { const pluginsElem = element.properties.find( @@ -557,7 +916,15 @@ export function removeCompatExtends( ts.ScriptKind.JS ); const changes: StringChange[] = []; - findAllBlocks(source)?.forEach((node) => { + const format = content.includes('export default') ? 'mjs' : 'cjs'; + const exportsArray = + format === 'mjs' ? findAllBlocks(source) : findModuleExports(source); + + if (!exportsArray) { + return content; + } + + exportsArray.forEach((node) => { if ( ts.isSpreadElement(node) && ts.isCallExpression(node.expression) && @@ -618,9 +985,16 @@ export function removePredefinedConfigs( true, ts.ScriptKind.JS ); + const format = content.includes('export default') ? 'mjs' : 'cjs'; const changes: StringChange[] = []; let removeImport = true; - findAllBlocks(source)?.forEach((node) => { + const exportsArray = + format === 'mjs' ? findAllBlocks(source) : findModuleExports(source); + if (!exportsArray) { + return content; + } + + exportsArray.forEach((node) => { if ( ts.isSpreadElement(node) && ts.isElementAccessExpression(node.expression) && @@ -683,18 +1057,27 @@ export function addPluginsToExportsBlock( * Adds compat if missing to flat config */ export function addFlatCompatToFlatConfig(content: string) { - let result = content; - result = addImportToFlatConfig(result, 'js', '@eslint/js'); + const result = addImportToFlatConfig(content, 'js', '@eslint/js'); + const format = content.includes('export default') ? 'mjs' : 'cjs'; if (result.includes('const compat = new FlatCompat')) { return result; } - result = addImportToFlatConfig(result, ['FlatCompat'], '@eslint/eslintrc'); - const index = result.indexOf('export default myExports;'); - return applyChangesToString(result, [ + + if (format === 'mjs') { + return addFlatCompatToFlatConfigESM(result); + } else { + return addFlatCompatToFlatConfigCJS(result); + } +} + +function addFlatCompatToFlatConfigCJS(content: string) { + content = addImportToFlatConfig(content, ['FlatCompat'], '@eslint/eslintrc'); + const index = content.indexOf('module.exports'); + return applyChangesToString(content, [ { type: ChangeType.Insert, index: index - 1, - text: `\n + text: ` const compat = new FlatCompat({ baseDirectory: __dirname, recommendedConfig: js.configs.recommended, @@ -703,6 +1086,32 @@ const compat = new FlatCompat({ }, ]); } +function addFlatCompatToFlatConfigESM(content: string) { + const importsToAdd = [ + { variable: 'js', module: '@eslint/js' }, + { variable: ['fileURLToPath'], module: 'url' }, + { variable: ['dirname'], module: 'path' }, + { variable: ['FlatCompat'], module: '@eslint/eslintrc' }, + ]; + + for (const { variable, module } of importsToAdd) { + content = addImportToFlatConfig(content, variable, module); + } + + const index = content.indexOf('export default'); + return applyChangesToString(content, [ + { + type: ChangeType.Insert, + index: index - 1, + text: ` +const compat = new FlatCompat({ + baseDirectory: dirname(fileURLToPath(import.meta.url)), + recommendedConfig: js.configs.recommended, +});\n +`, + }, + ]); +} /** * Generate node list representing the imports and the exports blocks @@ -710,16 +1119,26 @@ const compat = new FlatCompat({ */ export function createNodeList( importsMap: Map, - exportElements: ts.Expression[] + exportElements: ts.Expression[], + format: 'mjs' | 'cjs' ): ts.NodeArray< ts.VariableStatement | ts.Identifier | ts.ExpressionStatement | ts.SourceFile > { const importsList = []; Array.from(importsMap.entries()).forEach(([imp, varName]) => { - importsList.push(generateImport(varName, imp)); + if (format === 'mjs') { + importsList.push(generateESMImport(varName, imp)); + } else { + importsList.push(generateRequire(varName, imp)); + } }); + const exports = + format === 'mjs' + ? generateESMExport(exportElements) + : generateCJSExport(exportElements); + return ts.factory.createNodeArray([ // add plugin imports ...importsList, @@ -730,30 +1149,31 @@ export function createNodeList( false, ts.ScriptKind.JS ), + exports, + ]); +} - // creates: export const myExports = [...] - ts.factory.createVariableStatement( - [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], - ts.factory.createVariableDeclarationList( - [ - ts.factory.createVariableDeclaration( - ts.factory.createIdentifier('myExports'), - undefined, - undefined, - ts.factory.createArrayLiteralExpression(exportElements, true) - ), - ], - ts.NodeFlags.Const - ) - ), +function generateESMExport(elements: ts.Expression[]): ts.ExportAssignment { + // creates: export default = [...] + return ts.factory.createExportAssignment( + undefined, + false, + ts.factory.createArrayLiteralExpression(elements, true) + ); +} - // creates: export default myExports; - ts.factory.createExportAssignment( - undefined, - false, - ts.factory.createIdentifier('myExports') - ), - ]); +function generateCJSExport(elements: ts.Expression[]): ts.ExpressionStatement { + // creates: module.exports = [...] + return ts.factory.createExpressionStatement( + ts.factory.createBinaryExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('module'), + ts.factory.createIdentifier('exports') + ), + ts.factory.createToken(ts.SyntaxKind.EqualsToken), + ts.factory.createArrayLiteralExpression(elements, true) + ) + ); } export function generateSpreadElement(name: string): ts.SpreadElement { @@ -815,20 +1235,26 @@ export function stringifyNodeList( true, ts.ScriptKind.JS ); - return ( - printer - .printList(ts.ListFormat.MultiLine, nodes, resultFile) - // add new line before compat initialization - .replace( - /const compat = new FlatCompat/, - '\nconst compat = new FlatCompat' - ) - // add new line before module.exports = ... - .replace(/module\.exports/, '\nmodule.exports') - ); + const result = printer + .printList(ts.ListFormat.MultiLine, nodes, resultFile) + // add new line before compat initialization + .replace( + /const compat = new FlatCompat/, + '\nconst compat = new FlatCompat' + ); + + if (result.includes('export default')) { + return result // add new line before export default = ... + .replace(/export default/, '\nexport default'); + } else { + return result.replace(/module.exports/, '\nmodule.exports'); + } } -export function generateDynamicImport( +/** + * generates AST require statement + */ +export function generateRequire( variableName: string | ts.ObjectBindingPattern, imp: string ): ts.VariableStatement { @@ -840,12 +1266,10 @@ export function generateDynamicImport( variableName, undefined, undefined, - ts.factory.createAwaitExpression( - ts.factory.createCallExpression( - ts.factory.createIdentifier('import'), - undefined, - [ts.factory.createStringLiteral(imp)] - ) + ts.factory.createCallExpression( + ts.factory.createIdentifier('require'), + undefined, + [ts.factory.createStringLiteral(imp)] ) ), ], @@ -855,7 +1279,7 @@ export function generateDynamicImport( } // Top level imports -export function generateImport( +export function generateESMImport( variableName: string | ts.ObjectBindingPattern, imp: string ): ts.ImportDeclaration { @@ -931,7 +1355,8 @@ export function overrideNeedsCompat( export function generateFlatOverride( _override: Partial> & { ignores?: Linter.FlatConfig['ignores']; - } + }, + format: 'mjs' | 'cjs' ): ts.ObjectLiteralExpression | ts.SpreadElement { const override = mapFilePaths(_override); @@ -1009,22 +1434,9 @@ export function generateFlatOverride( return propertyAssignment; } else { // Change parser to import statement. - return ts.factory.createPropertyAssignment( - 'parser', - ts.factory.createAwaitExpression( - ts.factory.createCallExpression( - ts.factory.createIdentifier('import'), - undefined, - [ - ts.factory.createStringLiteral( - override['languageOptions']?.['parserOptions']?.parser ?? - override['languageOptions']?.parser ?? - override.parser - ), - ] - ) - ) - ); + return format === 'mjs' + ? generateESMParserImport(override) + : generateCJSParserImport(override); } }, }); @@ -1132,6 +1544,50 @@ export function generateFlatOverride( ); } +function generateESMParserImport( + override: Partial> & { + ignores?: Linter.FlatConfig['ignores']; + } +): ts.PropertyAssignment { + return ts.factory.createPropertyAssignment( + 'parser', + ts.factory.createAwaitExpression( + ts.factory.createCallExpression( + ts.factory.createIdentifier('import'), + undefined, + [ + ts.factory.createStringLiteral( + override['languageOptions']?.['parserOptions']?.parser ?? + override['languageOptions']?.parser ?? + override.parser + ), + ] + ) + ) + ); +} + +function generateCJSParserImport( + override: Partial> & { + ignores?: Linter.FlatConfig['ignores']; + } +): ts.PropertyAssignment { + return ts.factory.createPropertyAssignment( + 'parser', + ts.factory.createCallExpression( + ts.factory.createIdentifier('require'), + undefined, + [ + ts.factory.createStringLiteral( + override['languageOptions']?.['parserOptions']?.parser ?? + override['languageOptions']?.parser ?? + override.parser + ), + ] + ) + ); +} + export function generateFlatPredefinedConfig( predefinedConfigName: string, moduleName = 'nx', diff --git a/packages/next/src/generators/application/application.spec.ts b/packages/next/src/generators/application/application.spec.ts index 2ea0cbbdcde39..59495ff22bcd0 100644 --- a/packages/next/src/generators/application/application.spec.ts +++ b/packages/next/src/generators/application/application.spec.ts @@ -623,7 +623,40 @@ describe('app', () => { describe('--linter', () => { describe('default (eslint)', () => { - it('should add flat config as needed', async () => { + it('should add flat config as needed MJS', async () => { + tree.write('eslint.config.mjs', 'export default {};'); + const name = uniq(); + + await applicationGenerator(tree, { + directory: name, + style: 'css', + }); + + expect(tree.read(`${name}/eslint.config.mjs`, 'utf-8')) + .toMatchInlineSnapshot(` + "import { FlatCompat } from '@eslint/eslintrc'; + import { dirname } from 'path'; + import { fileURLToPath } from 'url'; + import js from '@eslint/js'; + import nx from '@nx/eslint-plugin'; + import baseConfig from '../eslint.config.mjs'; + const compat = new FlatCompat({ + baseDirectory: dirname(fileURLToPath(import.meta.url)), + recommendedConfig: js.configs.recommended, + }); + export default [ + ...compat.extends('next', 'next/core-web-vitals'), + ...baseConfig, + ...nx.configs['flat/react-typescript'], + { + ignores: ['.next/**/*'], + }, + ]; + " + `); + }); + + it('should add flat config as needed CJS', async () => { tree.write('eslint.config.cjs', ''); const name = uniq(); diff --git a/packages/nuxt/src/generators/application/__snapshots__/application.spec.ts.snap b/packages/nuxt/src/generators/application/__snapshots__/application.spec.ts.snap index e454561c696dc..8bbb33ee64d74 100644 --- a/packages/nuxt/src/generators/application/__snapshots__/application.spec.ts.snap +++ b/packages/nuxt/src/generators/application/__snapshots__/application.spec.ts.snap @@ -33,7 +33,7 @@ exports[`app generated files content - as-provided - my-app general application " `; -exports[`app generated files content - as-provided - my-app general application should configure eslint correctly (flat config) 1`] = ` +exports[`app generated files content - as-provided - my-app general application should configure eslint correctly (flat config CJS) 1`] = ` "const { FlatCompat } = require('@eslint/eslintrc'); const js = require('@eslint/js'); const baseConfig = require('../eslint.config.cjs'); @@ -66,6 +66,39 @@ module.exports = [ " `; +exports[`app generated files content - as-provided - my-app general application should configure eslint correctly (flat config ESM) 1`] = ` +"import { FlatCompat } from '@eslint/eslintrc'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import js from '@eslint/js'; +import baseConfig from '../eslint.config.mjs'; +const compat = new FlatCompat({ + baseDirectory: dirname(fileURLToPath(import.meta.url)), + recommendedConfig: js.configs.recommended, +}); +export default [ + ...baseConfig, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'], + // Override or add rules here + rules: {}, + }, + ...compat.extends('@nuxt/eslint-config'), + { + files: ['**/*.vue'], + languageOptions: { + parserOptions: { + parser: await import('@typescript-eslint/parser'), + }, + }, + }, + { + ignores: ['.nuxt/**', '.output/**', 'node_modules'], + }, +]; +" +`; + exports[`app generated files content - as-provided - my-app general application should configure nuxt correctly 1`] = ` "import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; import { defineNuxtConfig } from 'nuxt/config'; @@ -393,7 +426,7 @@ exports[`app generated files content - as-provided - myApp general application s " `; -exports[`app generated files content - as-provided - myApp general application should configure eslint correctly (flat config) 1`] = ` +exports[`app generated files content - as-provided - myApp general application should configure eslint correctly (flat config CJS) 1`] = ` "const { FlatCompat } = require('@eslint/eslintrc'); const js = require('@eslint/js'); const baseConfig = require('../eslint.config.cjs'); @@ -426,6 +459,39 @@ module.exports = [ " `; +exports[`app generated files content - as-provided - myApp general application should configure eslint correctly (flat config ESM) 1`] = ` +"import { FlatCompat } from '@eslint/eslintrc'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import js from '@eslint/js'; +import baseConfig from '../eslint.config.mjs'; +const compat = new FlatCompat({ + baseDirectory: dirname(fileURLToPath(import.meta.url)), + recommendedConfig: js.configs.recommended, +}); +export default [ + ...baseConfig, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'], + // Override or add rules here + rules: {}, + }, + ...compat.extends('@nuxt/eslint-config'), + { + files: ['**/*.vue'], + languageOptions: { + parserOptions: { + parser: await import('@typescript-eslint/parser'), + }, + }, + }, + { + ignores: ['.nuxt/**', '.output/**', 'node_modules'], + }, +]; +" +`; + exports[`app generated files content - as-provided - myApp general application should configure nuxt correctly 1`] = ` "import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; import { defineNuxtConfig } from 'nuxt/config'; diff --git a/packages/nuxt/src/generators/application/application.spec.ts b/packages/nuxt/src/generators/application/application.spec.ts index aece521c022c9..8b10a8b34b68f 100644 --- a/packages/nuxt/src/generators/application/application.spec.ts +++ b/packages/nuxt/src/generators/application/application.spec.ts @@ -65,8 +65,21 @@ describe('app', () => { ).toMatchSnapshot(); }); - it('should configure eslint correctly (flat config)', async () => { - tree.write('eslint.config.cjs', ''); + it('should configure eslint correctly (flat config ESM)', async () => { + tree.write('eslint.config.mjs', 'export default {};'); + + await applicationGenerator(tree, { + directory: name, + unitTestRunner: 'vitest', + }); + + expect( + tree.read(`${name}/eslint.config.mjs`, 'utf-8') + ).toMatchSnapshot(); + }); + + it('should configure eslint correctly (flat config CJS)', async () => { + tree.write('eslint.config.cjs', 'module.exports = {};'); await applicationGenerator(tree, { directory: name, diff --git a/packages/vue/src/generators/library/__snapshots__/library.spec.ts.snap b/packages/vue/src/generators/library/__snapshots__/library.spec.ts.snap index 89fd88a7b05fa..f32eaf7320e23 100644 --- a/packages/vue/src/generators/library/__snapshots__/library.spec.ts.snap +++ b/packages/vue/src/generators/library/__snapshots__/library.spec.ts.snap @@ -277,7 +277,7 @@ exports[`library should ignore test files in tsconfig.lib.json 1`] = ` ] `; -exports[`library should support eslint flat config 1`] = ` +exports[`library should support eslint flat config CJS 1`] = ` "const vue = require('eslint-plugin-vue'); const baseConfig = require('../eslint.config.cjs'); @@ -301,3 +301,27 @@ module.exports = [ ]; " `; + +exports[`library should support eslint flat config ESM 1`] = ` +"import vue from 'eslint-plugin-vue'; +import baseConfig from '../eslint.config.mjs'; +export default [ + ...baseConfig, + ...vue.configs['flat/recommended'], + { + files: ['**/*.vue'], + languageOptions: { + parserOptions: { + parser: await import('@typescript-eslint/parser'), + }, + }, + }, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'], + rules: { + 'vue/multi-word-component-names': 'off', + }, + }, +]; +" +`; diff --git a/packages/vue/src/generators/library/library.spec.ts b/packages/vue/src/generators/library/library.spec.ts index 73ee4dea0232b..01ac046ee4fe4 100644 --- a/packages/vue/src/generators/library/library.spec.ts +++ b/packages/vue/src/generators/library/library.spec.ts @@ -149,7 +149,7 @@ describe('library', () => { expect(eslintJson).toMatchSnapshot(); }); - it('should support eslint flat config', async () => { + it('should support eslint flat config CJS', async () => { tree.write( 'eslint.config.cjs', `const { FlatCompat } = require('@eslint/eslintrc'); @@ -217,6 +217,76 @@ module.exports = [ ); }); + it('should support eslint flat config ESM', async () => { + tree.write( + 'eslint.config.mjs', + `import { FlatCompat } from '@eslint/eslintrc'; + import { dirname } from 'path'; + import { fileURLToPath } from 'url'; + import js from '@eslint/js'; + import nx from '@nx/eslint-plugin'; + import baseConfig from '../eslint.config.mjs'; + + const compat = new FlatCompat({ + baseDirectory: dirname(fileURLToPath(import.meta.url)), + recommendedConfig: js.configs.recommended, + }); + + export default [ + { plugins: { '@nx': nxEslintPlugin } }, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + rules: { + '@nx/enforce-module-boundaries': [ + 'error', + { + enforceBuildableLibDependency: true, + allow: [], + depConstraints: [ + { + sourceTag: '*', + onlyDependOnLibsWithTags: ['*'], + }, + ], + }, + ], + }, + }, + ...compat.config({ extends: ['plugin:@nx/typescript'] }).map((config) => ({ + ...config, + files: ['**/*.ts', '**/*.tsx'], + rules: { + ...config.rules, + }, + })), + ...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({ + ...config, + files: ['**/*.js', '**/*.jsx'], + rules: { + ...config.rules, + }, + })), + ...compat.config({ env: { jest: true } }).map((config) => ({ + ...config, + files: ['**/*.spec.ts', '**/*.spec.tsx', '**/*.spec.js', '**/*.spec.jsx'], + rules: { + ...config.rules, + }, + })), +]` + ); + + await libraryGenerator(tree, defaultSchema); + + const eslintJson = tree.read('my-lib/eslint.config.mjs', 'utf-8'); + expect(eslintJson).toMatchSnapshot(); + // assert **/*.vue was added to override in base eslint config + const eslintBaseJson = tree.read('eslint.config.mjs', 'utf-8'); + expect(eslintBaseJson).toContain( + `files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'],` + ); + }); + describe('nested', () => { it('should update tags and implicitDependencies', async () => { await libraryGenerator(tree, {