Skip to content

Commit

Permalink
feat(linter): add create-nodes plugin (#20264)
Browse files Browse the repository at this point in the history
  • Loading branch information
meeroslav authored Nov 30, 2023
1 parent a395fd3 commit fe63f85
Show file tree
Hide file tree
Showing 16 changed files with 587 additions and 103 deletions.
39 changes: 39 additions & 0 deletions e2e/eslint/src/linter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
});
});
});
});

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
}
Expand Down Expand Up @@ -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)[]
Expand Down
4 changes: 2 additions & 2 deletions packages/eslint/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
1 change: 1 addition & 0 deletions packages/eslint/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { createNodes, EslintPluginOptions } from './src/plugins/plugin';
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down
118 changes: 82 additions & 36 deletions packages/eslint/src/generators/init/init.spec.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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,
});

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 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);
});

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",
},
]
`);
});
});
66 changes: 53 additions & 13 deletions packages/eslint/src/generators/init/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}

nxJson.targetDefaults ??= {};
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`,
Expand All @@ -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.
*/
Expand All @@ -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,
Expand Down
Loading

0 comments on commit fe63f85

Please sign in to comment.