feat(linter): add create-nodes plugin (#20264)

This commit is contained in:
Miroslav Jonaš 2023-11-30 15:15:08 +01:00 committed by GitHub
parent a395fd3c4e
commit fe63f856ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 587 additions and 103 deletions

View File

@ -764,6 +764,45 @@ describe('Linter', () => {
expect(e2eEslint.overrides[0].extends).toBeUndefined();
});
});
describe('Project Config v3', () => {
let myapp;
beforeEach(() => {
myapp = uniq('myapp');
newProject({
name: uniq('eslint'),
unsetProjectNameAndRootFormat: false,
});
});
it('should lint example app', () => {
runCLI(
`generate @nx/react:app ${myapp} --directory apps/${myapp} --unitTestRunner=none --bundler=vite --e2eTestRunner=cypress --style=css --no-interactive --projectNameAndRootFormat=as-provided`,
{ env: { NX_PCV3: 'true' } }
);
let lintResults = runCLI(`lint ${myapp}`);
expect(lintResults).toContain(
`Successfully ran target lint for project ${myapp}`
);
lintResults = runCLI(`lint ${myapp}-e2e`);
expect(lintResults).toContain(
`Successfully ran target lint for project ${myapp}-e2e`
);
const { targets } = readJson(`apps/${myapp}/project.json`);
expect(targets.lint).not.toBeDefined();
const { plugins } = readJson('nx.json');
expect(plugins).toContainEqual({
plugin: '@nx/eslint/plugin',
options: {
targetName: 'lint',
},
});
});
});
});
/**

View File

@ -134,7 +134,7 @@ function removeConfigurationDefinedByPlugin<T>(
for (const [optionName, optionValue] of Object.entries(
targetFromProjectConfig.options ?? {}
)) {
if (targetFromCreateNodes.options[optionName] === optionValue) {
if (equals(targetFromCreateNodes.options[optionName], optionValue)) {
delete targetFromProjectConfig.options[optionName];
}
}
@ -167,6 +167,16 @@ function removeConfigurationDefinedByPlugin<T>(
}
}
function equals<T extends unknown>(a: T, b: T) {
if (Array.isArray(a) && Array.isArray(b)) {
return a.length === b.length && a.every((v, i) => v === b[i]);
}
if (typeof a === 'object' && typeof b === 'object') {
return hashObject(a) === hashObject(b);
}
return a === b;
}
function shouldRemoveArrayProperty(
arrayValuesFromProjectConfiguration: (object | string)[],
arrayValuesFromCreateNodes: (object | string)[]

View File

@ -27,16 +27,16 @@
"requirements": {},
"migrations": "./migrations.json"
},
"executors": "./executors.json",
"generators": "./generators.json",
"executors": "./executors.json",
"peerDependencies": {
"eslint": "^8.0.0",
"js-yaml": "4.1.0"
},
"dependencies": {
"tslib": "^2.3.0",
"@nx/devkit": "file:../devkit",
"@nx/js": "file:../js",
"tslib": "^2.3.0",
"typescript": "~5.2.2"
},
"peerDependenciesMeta": {

View File

@ -0,0 +1 @@
export { createNodes, EslintPluginOptions } from './src/plugins/plugin';

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`@nx/eslint:init --linter eslint should generate the global eslint config 1`] = `
exports[`@nx/eslint:init should generate the global eslint config 1`] = `
"{
"root": true,
"ignorePatterns": [

View File

@ -1,5 +1,5 @@
import { Linter } from '../utils/linter';
import { readJson, Tree } from '@nx/devkit';
import { NxJsonConfiguration, readJson, Tree, updateJson } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { lintInitGenerator } from './init';
@ -10,45 +10,91 @@ describe('@nx/eslint:init', () => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
});
describe('--linter', () => {
describe('eslint', () => {
it('should generate the global eslint config', async () => {
await lintInitGenerator(tree, {
linter: Linter.EsLint,
});
it('should generate the global eslint config', async () => {
await lintInitGenerator(tree, {
linter: Linter.EsLint,
});
expect(tree.read('.eslintrc.json', 'utf-8')).toMatchSnapshot();
expect(tree.read('.eslintignore', 'utf-8')).toMatchInlineSnapshot(`
expect(tree.read('.eslintrc.json', 'utf-8')).toMatchSnapshot();
expect(tree.read('.eslintignore', 'utf-8')).toMatchInlineSnapshot(`
"node_modules
"
`);
});
});
it('should add the root eslint config to the lint targetDefaults for lint', async () => {
await lintInitGenerator(tree, {
linter: Linter.EsLint,
});
it('should add the root eslint config to the lint targetDefaults for lint', async () => {
await lintInitGenerator(tree, {
linter: Linter.EsLint,
});
expect(readJson(tree, 'nx.json').targetDefaults.lint).toEqual({
cache: true,
inputs: [
'default',
'{workspaceRoot}/.eslintrc.json',
'{workspaceRoot}/.eslintignore',
'{workspaceRoot}/eslint.config.js',
],
});
});
it('should not generate the global eslint config if it already exist', async () => {
tree.write('.eslintrc.js', '{}');
await lintInitGenerator(tree, {
linter: Linter.EsLint,
});
expect(tree.exists('.eslintrc.json')).toBe(false);
});
expect(readJson(tree, 'nx.json').targetDefaults.lint).toEqual({
cache: true,
inputs: [
'default',
'{workspaceRoot}/.eslintrc.json',
'{workspaceRoot}/.eslintignore',
'{workspaceRoot}/eslint.config.js',
],
});
});
it('should not generate the global eslint config if it already exist', async () => {
tree.write('.eslintrc.js', '{}');
await lintInitGenerator(tree, {
linter: Linter.EsLint,
});
expect(tree.exists('.eslintrc.json')).toBe(false);
});
it('should setup lint target defaults', async () => {
updateJson<NxJsonConfiguration>(tree, 'nx.json', (json) => {
json.namedInputs ??= {};
json.namedInputs.production = ['default'];
return json;
});
await lintInitGenerator(tree, {});
expect(
readJson<NxJsonConfiguration>(tree, 'nx.json').targetDefaults.lint
).toEqual({
cache: true,
inputs: [
'default',
'{workspaceRoot}/.eslintrc.json',
'{workspaceRoot}/.eslintignore',
'{workspaceRoot}/eslint.config.js',
],
});
});
it('should setup @nx/eslint/plugin', async () => {
process.env.NX_PCV3 = 'true';
updateJson<NxJsonConfiguration>(tree, 'nx.json', (json) => {
json.namedInputs ??= {};
json.namedInputs.production = ['default'];
return json;
});
await lintInitGenerator(tree, {});
expect(
readJson<NxJsonConfiguration>(tree, 'nx.json').targetDefaults.lint
).toEqual({
cache: true,
});
expect(readJson<NxJsonConfiguration>(tree, 'nx.json').plugins)
.toMatchInlineSnapshot(`
[
{
"options": {
"targetName": "lint",
},
"plugin": "@nx/eslint/plugin",
},
]
`);
});
});

View File

@ -17,6 +17,7 @@ import {
import { Linter } from '../utils/linter';
import { findEslintFile } from '../utils/eslint-file';
import { getGlobalEsLintConfiguration } from './global-eslint-config';
import { EslintPluginOptions } from '../../plugins/plugin';
export interface LinterInitOptions {
linter?: Linter;
@ -25,21 +26,25 @@ export interface LinterInitOptions {
rootProject?: boolean;
}
function addTargetDefaults(tree: Tree) {
function updateProductionFileset(tree: Tree) {
const nxJson = readNxJson(tree);
const productionFileSet = nxJson.namedInputs?.production;
if (productionFileSet) {
// Remove .eslintrc.json
productionFileSet.push('!{projectRoot}/.eslintrc.json');
productionFileSet.push('!{projectRoot}/eslint.config.js');
// Dedupe and set
nxJson.namedInputs.production = Array.from(new Set(productionFileSet));
}
updateNxJson(tree, nxJson);
}
function addTargetDefaults(tree: Tree) {
const nxJson = readNxJson(tree);
nxJson.targetDefaults ??= {};
nxJson.targetDefaults.lint ??= {};
nxJson.targetDefaults.lint.cache ??= true;
nxJson.targetDefaults.lint.inputs ??= [
'default',
`{workspaceRoot}/.eslintrc.json`,
@ -49,6 +54,42 @@ function addTargetDefaults(tree: Tree) {
updateNxJson(tree, nxJson);
}
function addPlugin(tree: Tree) {
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
for (const plugin of nxJson.plugins) {
if (
typeof plugin === 'string'
? plugin === '@nx/eslint/plugin'
: plugin.plugin === '@nx/eslint/plugin'
) {
return;
}
}
nxJson.plugins.push({
plugin: '@nx/eslint/plugin',
options: {
targetName: 'lint',
} as EslintPluginOptions,
});
updateNxJson(tree, nxJson);
}
function updateVSCodeExtensions(tree: Tree) {
if (tree.exists('.vscode/extensions.json')) {
updateJson(tree, '.vscode/extensions.json', (json) => {
json.recommendations ||= [];
const extension = 'dbaeumer.vscode-eslint';
if (!json.recommendations.includes(extension)) {
json.recommendations.push(extension);
}
return json;
});
}
}
/**
* Initializes ESLint configuration in a workspace and adds necessary dependencies.
*/
@ -67,19 +108,18 @@ function initEsLint(tree: Tree, options: LinterInitOptions): GeneratorCallback {
getGlobalEsLintConfiguration(options.unitTestRunner, options.rootProject)
);
tree.write('.eslintignore', 'node_modules\n');
addTargetDefaults(tree);
if (tree.exists('.vscode/extensions.json')) {
updateJson(tree, '.vscode/extensions.json', (json) => {
json.recommendations ||= [];
const extension = 'dbaeumer.vscode-eslint';
if (!json.recommendations.includes(extension)) {
json.recommendations.push(extension);
}
return json;
});
updateProductionFileset(tree);
const addPlugins = process.env.NX_PCV3 === 'true';
if (addPlugins) {
addPlugin(tree);
} else {
addTargetDefaults(tree);
}
updateVSCodeExtensions(tree);
return !options.skipPackageJson
? addDependenciesToPackageJson(
tree,

View File

@ -14,11 +14,7 @@ import {
} from '@nx/devkit';
import { Linter as LinterEnum } from '../utils/linter';
import {
baseEsLintConfigFile,
baseEsLintFlatConfigFile,
findEslintFile,
} from '../utils/eslint-file';
import { findEslintFile } from '../utils/eslint-file';
import { join } from 'path';
import { lintInitGenerator } from '../init/init';
import type { Linter } from 'eslint';
@ -34,6 +30,10 @@ import {
generateSpreadElement,
stringifyNodeList,
} from '../utils/flat-config/ast-utils';
import {
baseEsLintConfigFile,
baseEsLintFlatConfigFile,
} from '../../utils/config-file';
interface LintProjectOptions {
project: string;
@ -59,27 +59,46 @@ export async function lintProjectGenerator(
});
const projectConfig = readProjectConfiguration(tree, options.project);
projectConfig.targets['lint'] = {
executor: '@nx/eslint:lint',
outputs: ['{options.outputFile}'],
};
let lintFilePatterns = options.eslintFilePatterns;
if (!lintFilePatterns && options.rootProject && projectConfig.root === '.') {
lintFilePatterns = ['./src'];
}
if (lintFilePatterns && lintFilePatterns.length) {
if (
isBuildableLibraryProject(projectConfig) &&
!lintFilePatterns.includes('{projectRoot}')
) {
lintFilePatterns.push(`{projectRoot}/package.json`);
}
if (
lintFilePatterns &&
lintFilePatterns.length &&
!lintFilePatterns.includes('{projectRoot}') &&
isBuildableLibraryProject(projectConfig)
) {
lintFilePatterns.push(`{projectRoot}/package.json`);
}
// only add lintFilePatterns if they are explicitly defined
projectConfig.targets['lint'].options = {
lintFilePatterns,
const usePlugin = process.env.NX_PCV3 === 'true';
if (usePlugin) {
if (
lintFilePatterns &&
lintFilePatterns.length &&
lintFilePatterns.some(
(p) => !['./src', '{projectRoot}', projectConfig.root].includes(p)
)
) {
projectConfig.targets['lint'] = {
command: `eslint ${lintFilePatterns
.join(' ')
.replace('{projectRoot}', projectConfig.root)}`,
};
}
} else {
projectConfig.targets['lint'] = {
executor: '@nx/eslint:lint',
outputs: ['{options.outputFile}'],
};
if (lintFilePatterns && lintFilePatterns.length) {
// only add lintFilePatterns if they are explicitly defined
projectConfig.targets['lint'].options = {
lintFilePatterns,
};
}
}
// we are adding new project which is not the root project or

View File

@ -1,13 +1,15 @@
import {
addExtendsToLintConfig,
baseEsLintConfigFile,
eslintConfigFileWhitelist,
findEslintFile,
lintConfigHasOverride,
} from './eslint-file';
import { Tree, readJson } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import {
ESLINT_CONFIG_FILENAMES,
baseEsLintConfigFile,
} from '../../utils/config-file';
describe('@nx/eslint:lint-file', () => {
let tree: Tree;
@ -21,7 +23,7 @@ describe('@nx/eslint:lint-file', () => {
expect(findEslintFile(tree)).toBe(null);
});
test.each(eslintConfigFileWhitelist)(
test.each(ESLINT_CONFIG_FILENAMES)(
'should return %p when calling findEslintFile',
(eslintFileName) => {
tree.write(eslintFileName, '{}');
@ -29,7 +31,7 @@ describe('@nx/eslint:lint-file', () => {
}
);
test.each(eslintConfigFileWhitelist)(
test.each(ESLINT_CONFIG_FILENAMES)(
'should return base file instead %p when calling findEslintFile',
(eslintFileName) => {
tree.write(baseEsLintConfigFile, '{}');

View File

@ -22,28 +22,24 @@ import {
} from './flat-config/ast-utils';
import ts = require('typescript');
import { mapFilePath } from './flat-config/path-utils';
import {
baseEsLintConfigFile,
baseEsLintFlatConfigFile,
ESLINT_CONFIG_FILENAMES,
} from '../../utils/config-file';
export const eslintConfigFileWhitelist = [
'.eslintrc',
'.eslintrc.js',
'.eslintrc.cjs',
'.eslintrc.yaml',
'.eslintrc.yml',
'.eslintrc.json',
'eslint.config.js',
];
export const baseEsLintConfigFile = '.eslintrc.base.json';
export const baseEsLintFlatConfigFile = 'eslint.base.config.js';
export function findEslintFile(tree: Tree, projectRoot = ''): string | null {
if (projectRoot === '' && tree.exists(baseEsLintConfigFile)) {
export function findEslintFile(
tree: Tree,
projectRoot?: string
): string | null {
if (projectRoot === undefined && tree.exists(baseEsLintConfigFile)) {
return baseEsLintConfigFile;
}
if (projectRoot === '' && tree.exists(baseEsLintFlatConfigFile)) {
if (projectRoot === undefined && tree.exists(baseEsLintFlatConfigFile)) {
return baseEsLintFlatConfigFile;
}
for (const file of eslintConfigFileWhitelist) {
projectRoot ??= '';
for (const file of ESLINT_CONFIG_FILENAMES) {
if (tree.exists(joinPathFragments(projectRoot, file))) {
return file;
}
@ -148,7 +144,7 @@ function offsetFilePath(
tree: Tree
): string {
if (
eslintConfigFileWhitelist.some((eslintFile) =>
ESLINT_CONFIG_FILENAMES.some((eslintFile) =>
pathToFile.includes(eslintFile)
)
) {

View File

@ -6,7 +6,7 @@ import {
updateNxJson,
} from '@nx/devkit';
import addEslintInputs from './add-eslint-inputs';
import { eslintConfigFileWhitelist } from '../../generators/utils/eslint-file';
import { ESLINT_CONFIG_FILENAMES } from '../../utils/config-file';
describe('15.0.0 migration (add-eslint-inputs)', () => {
let tree: Tree;
@ -41,7 +41,7 @@ describe('15.0.0 migration (add-eslint-inputs)', () => {
});
});
test.each(eslintConfigFileWhitelist)(
test.each(ESLINT_CONFIG_FILENAMES)(
'should ignore %p for production',
async (eslintConfigFilename) => {
tree.write(eslintConfigFilename, '{}');
@ -57,7 +57,7 @@ describe('15.0.0 migration (add-eslint-inputs)', () => {
}
);
test.each(eslintConfigFileWhitelist)(
test.each(ESLINT_CONFIG_FILENAMES)(
'should add %p to all lint targets',
async (eslintConfigFilename) => {
tree.write(eslintConfigFilename, '{}');
@ -95,7 +95,7 @@ describe('15.0.0 migration (add-eslint-inputs)', () => {
});
});
test.each(eslintConfigFileWhitelist)(
test.each(ESLINT_CONFIG_FILENAMES)(
'should not add `!{projectRoot}/%s` if `workspaceConfiguration.namedInputs` is undefined',
async (eslintConfigFilename) => {
tree.write(eslintConfigFilename, '{}');
@ -108,7 +108,7 @@ describe('15.0.0 migration (add-eslint-inputs)', () => {
}
);
test.each(eslintConfigFileWhitelist)(
test.each(ESLINT_CONFIG_FILENAMES)(
'should not add `!{projectRoot}/%s` if `workspaceConfiguration.namedInputs.production` is undefined',
async (eslintConfigFilename) => {
updateNxJson(tree, {
@ -148,7 +148,7 @@ describe('15.0.0 migration (add-eslint-inputs)', () => {
});
});
test.each(eslintConfigFileWhitelist)(
test.each(ESLINT_CONFIG_FILENAMES)(
'should not override `targetDefaults.lint.inputs` with `%s` as there was a default target set in the workspace config',
async (eslintConfigFilename) => {
updateNxJson(tree, {

View File

@ -5,13 +5,13 @@ import {
Tree,
updateNxJson,
} from '@nx/devkit';
import { eslintConfigFileWhitelist } from '../../generators/utils/eslint-file';
import { getEslintTargets } from '../../generators/utils/eslint-targets';
import { ESLINT_CONFIG_FILENAMES } from '../../utils/config-file';
export default async function addEslintInputs(tree: Tree) {
const nxJson = readNxJson(tree);
const globalEslintFile = eslintConfigFileWhitelist.find((file) =>
const globalEslintFile = ESLINT_CONFIG_FILENAMES.find((file) =>
tree.exists(file)
);

View File

@ -5,13 +5,13 @@ import {
Tree,
updateNxJson,
} from '@nx/devkit';
import { eslintConfigFileWhitelist } from '../../generators/utils/eslint-file';
import { getEslintTargets } from '../../generators/utils/eslint-targets';
import { ESLINT_CONFIG_FILENAMES } from '../../utils/config-file';
export default async function addEslintIgnore(tree: Tree) {
const nxJson = readJson(tree, 'nx.json');
const globalEslintFile = eslintConfigFileWhitelist.find((file) =>
const globalEslintFile = ESLINT_CONFIG_FILENAMES.find((file) =>
tree.exists(file)
);

View File

@ -0,0 +1,147 @@
import { CreateNodesContext } from '@nx/devkit';
import { createNodes } from './plugin';
import { vol } from 'memfs';
jest.mock('fs', () => {
const memFs = require('memfs').fs;
return {
...memFs,
existsSync: (p) => (p.endsWith('.node') ? true : memFs.existsSync(p)),
};
});
describe('@nx/eslint/plugin', () => {
let createNodesFunction = createNodes[1];
let context: CreateNodesContext;
beforeEach(async () => {
context = {
nxJsonConfiguration: {
namedInputs: {
default: ['{projectRoot}/**/*'],
production: ['!{projectRoot}/**/*.spec.ts'],
},
},
workspaceRoot: '',
};
});
afterEach(() => {
vol.reset();
jest.resetModules();
});
it('should create nodes with default configuration for nested project', () => {
const fileSys = {
'apps/my-app/.eslintrc.json': `{}`,
'apps/my-app/project.json': `{}`,
'.eslintrc.json': `{}`,
'package.json': `{}`,
};
vol.fromJSON(fileSys, '');
const nodes = createNodesFunction(
'apps/my-app/project.json',
{
targetName: 'lint',
},
context
);
expect(nodes).toMatchInlineSnapshot(`
{
"projects": {
"apps/my-app": {
"targets": {
"lint": {
"cache": true,
"command": "eslint .",
"inputs": [
"default",
"{workspaceRoot}/.eslintrc.json",
"{workspaceRoot}/apps/my-app/.eslintrc.json",
"{workspaceRoot}/tools/eslint-rules/**/*",
{
"externalDependencies": [
"eslint",
],
},
],
"options": {
"cwd": "apps/my-app",
},
},
},
},
},
}
`);
});
it('should create nodes with default configuration for standalone project', () => {
const fileSys = {
'apps/my-app/eslint.config.js': `module.exports = []`,
'apps/my-app/project.json': `{}`,
'eslint.config.js': `module.exports = []`,
'src/index.ts': `console.log('hello world')`,
'package.json': `{}`,
};
vol.fromJSON(fileSys, '');
const nodes = createNodesFunction(
'package.json',
{
targetName: 'lint',
},
context
);
expect(nodes).toMatchInlineSnapshot(`
{
"projects": {
".": {
"targets": {
"lint": {
"cache": true,
"command": "eslint ./src",
"inputs": [
"default",
"{workspaceRoot}/eslint.config.js",
"{workspaceRoot}/tools/eslint-rules/**/*",
{
"externalDependencies": [
"eslint",
],
},
],
"options": {
"cwd": ".",
"env": {
"ESLINT_USE_FLAT_CONFIG": "true",
},
},
},
},
},
},
}
`);
});
it('should not create nodes if no src folder for root', () => {
const fileSys = {
'apps/my-app/eslint.config.js': `module.exports = []`,
'apps/my-app/project.json': `{}`,
'eslint.config.js': `module.exports = []`,
'package.json': `{}`,
};
vol.fromJSON(fileSys, '');
const nodes = createNodesFunction(
'package.json',
{
targetName: 'lint',
},
context
);
expect(nodes).toMatchInlineSnapshot(`{}`);
});
});

View File

@ -0,0 +1,149 @@
import {
CreateNodes,
CreateNodesContext,
TargetConfiguration,
} from '@nx/devkit';
import { dirname, join } from 'path';
import { readTargetDefaultsForTarget } from 'nx/src/project-graph/utils/project-configuration-utils';
import { readdirSync } from 'fs';
import { combineGlobPatterns } from 'nx/src/utils/globs';
import {
ESLINT_CONFIG_FILENAMES,
findBaseEslintFile,
isFlatConfig,
} from '../utils/config-file';
export interface EslintPluginOptions {
targetName?: string;
}
export const createNodes: CreateNodes<EslintPluginOptions> = [
combineGlobPatterns(['**/project.json', '**/package.json']),
(configFilePath, options, context) => {
const projectRoot = dirname(configFilePath);
options = normalizeOptions(options);
const eslintConfigs = getEslintConfigsForProject(
projectRoot,
context.workspaceRoot
);
if (!eslintConfigs.length) {
return {};
}
return {
projects: {
[projectRoot]: {
targets: buildEslintTargets(
eslintConfigs,
projectRoot,
options,
context
),
},
},
};
},
];
function getEslintConfigsForProject(
projectRoot: string,
workspaceRoot: string
): string[] {
const detectedConfigs = new Set<string>();
const baseConfig = findBaseEslintFile(workspaceRoot);
if (baseConfig) {
detectedConfigs.add(baseConfig);
}
let siblingFiles = readdirSync(join(workspaceRoot, projectRoot));
if (projectRoot === '.') {
// If there's no src folder, it's not a standalone project
if (!siblingFiles.includes('src')) {
return [];
}
// If it's standalone but doesn't have eslint config, it's not a lintable
const config = siblingFiles.find((f) =>
ESLINT_CONFIG_FILENAMES.includes(f)
);
if (!config) {
return [];
}
detectedConfigs.add(config);
return Array.from(detectedConfigs);
}
while (projectRoot !== '.') {
// if it has an eslint config it's lintable
const config = siblingFiles.find((f) =>
ESLINT_CONFIG_FILENAMES.includes(f)
);
if (config) {
detectedConfigs.add(`${projectRoot}/${config}`);
return Array.from(detectedConfigs);
}
projectRoot = dirname(projectRoot);
siblingFiles = readdirSync(join(workspaceRoot, projectRoot));
}
// check whether the root has an eslint config
const config = readdirSync(workspaceRoot).find((f) =>
ESLINT_CONFIG_FILENAMES.includes(f)
);
if (config) {
detectedConfigs.add(config);
return Array.from(detectedConfigs);
}
return [];
}
function buildEslintTargets(
eslintConfigs: string[],
projectRoot: string,
options: EslintPluginOptions,
context: CreateNodesContext
) {
const targetDefaults = readTargetDefaultsForTarget(
options.targetName,
context.nxJsonConfiguration.targetDefaults,
'@nx/eslint:lint'
);
const isRootProject = projectRoot === '.';
const targets: Record<string, TargetConfiguration> = {};
const baseTargetConfig: TargetConfiguration = {
command: `eslint ${isRootProject ? './src' : '.'}`,
options: {
cwd: projectRoot,
},
};
if (eslintConfigs.some((config) => isFlatConfig(config))) {
baseTargetConfig.options.env = {
ESLINT_USE_FLAT_CONFIG: 'true',
};
}
targets[options.targetName] = {
...baseTargetConfig,
cache: targetDefaults?.cache ?? true,
inputs: targetDefaults?.inputs ?? [
'default',
...eslintConfigs.map((config) => `{workspaceRoot}/${config}`),
'{workspaceRoot}/tools/eslint-rules/**/*',
{ externalDependencies: ['eslint'] },
],
options: {
...baseTargetConfig.options,
},
};
return targets;
}
function normalizeOptions(options: EslintPluginOptions): EslintPluginOptions {
options ??= {};
options.targetName ??= 'lint';
return options;
}

View File

@ -0,0 +1,35 @@
import { joinPathFragments } from '@nx/devkit';
import { existsSync } from 'fs';
export const ESLINT_CONFIG_FILENAMES = [
'.eslintrc',
'.eslintrc.js',
'.eslintrc.cjs',
'.eslintrc.yaml',
'.eslintrc.yml',
'.eslintrc.json',
'eslint.config.js',
];
export const baseEsLintConfigFile = '.eslintrc.base.json';
export const baseEsLintFlatConfigFile = 'eslint.base.config.js';
export function findBaseEslintFile(workspaceRoot = ''): string | null {
if (existsSync(joinPathFragments(workspaceRoot, baseEsLintConfigFile))) {
return baseEsLintConfigFile;
}
if (existsSync(joinPathFragments(workspaceRoot, baseEsLintFlatConfigFile))) {
return baseEsLintFlatConfigFile;
}
for (const file of ESLINT_CONFIG_FILENAMES) {
if (existsSync(joinPathFragments(workspaceRoot, file))) {
return file;
}
}
return null;
}
export function isFlatConfig(configFilePath: string): boolean {
return configFilePath.endsWith('.config.js');
}