feat(linter): add create-nodes plugin (#20264)
This commit is contained in:
parent
a395fd3c4e
commit
fe63f856ec
@ -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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@ -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)[]
|
||||
|
||||
@ -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": {
|
||||
|
||||
1
packages/eslint/plugin.ts
Normal file
1
packages/eslint/plugin.ts
Normal file
@ -0,0 +1 @@
|
||||
export { createNodes, EslintPluginOptions } from './src/plugins/plugin';
|
||||
@ -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": [
|
||||
|
||||
@ -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",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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, '{}');
|
||||
|
||||
@ -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)
|
||||
)
|
||||
) {
|
||||
|
||||
@ -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, {
|
||||
|
||||
@ -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)
|
||||
);
|
||||
|
||||
|
||||
@ -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)
|
||||
);
|
||||
|
||||
|
||||
147
packages/eslint/src/plugins/plugin.spec.ts
Normal file
147
packages/eslint/src/plugins/plugin.spec.ts
Normal 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(`{}`);
|
||||
});
|
||||
});
|
||||
149
packages/eslint/src/plugins/plugin.ts
Normal file
149
packages/eslint/src/plugins/plugin.ts
Normal 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;
|
||||
}
|
||||
35
packages/eslint/src/utils/config-file.ts
Normal file
35
packages/eslint/src/utils/config-file.ts
Normal 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');
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user