Skip to content

Commit

Permalink
feat(core): enable linter on root projects (#13347)
Browse files Browse the repository at this point in the history
  • Loading branch information
meeroslav committed Nov 24, 2022
1 parent 8200870 commit 110b5f2
Show file tree
Hide file tree
Showing 13 changed files with 398 additions and 95 deletions.
72 changes: 72 additions & 0 deletions e2e/linter/src/linter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,78 @@ export function tslibC(): string {
);
});
});

describe('Root projects migration', () => {
afterEach(() => cleanupProject());

it('should set root project config to app and e2e app and migrate when another lib is added', () => {
const myapp = uniq('myapp');
const mylib = uniq('mylib');

newProject();
runCLI(`generate @nrwl/react:app ${myapp} --rootProject=true`);

let rootEslint = readJson('.eslintrc.json');
let e2eEslint = readJson('e2e/.eslintrc.json');
expect(() => checkFilesExist(`.eslintrc.${myapp}.json`)).toThrow();

// should directly refer to nx plugin
expect(rootEslint.plugins).toEqual(['@nrwl/nx']);
expect(e2eEslint.plugins).toEqual(['@nrwl/nx']);
// should only extend framework plugin
expect(rootEslint.extends).toEqual(['plugin:@nrwl/nx/react']);
expect(e2eEslint.extends).toEqual(['plugin:cypress/recommended']);
// should have plugin extends
expect(rootEslint.overrides[0].extends).toEqual([
'plugin:@nrwl/nx/typescript',
]);
expect(rootEslint.overrides[1].extends).toEqual([
'plugin:@nrwl/nx/javascript',
]);
expect(e2eEslint.overrides[0].extends).toEqual([
'plugin:@nrwl/nx/typescript',
]);
expect(e2eEslint.overrides[1].extends).toEqual([
'plugin:@nrwl/nx/javascript',
]);

console.log(JSON.stringify(rootEslint, null, 2));
console.log(JSON.stringify(e2eEslint, null, 2));

runCLI(`generate @nrwl/react:lib ${mylib}`);
// should add new tslint
expect(() => checkFilesExist(`.eslintrc.${myapp}.json`)).not.toThrow();
const appEslint = readJson(`.eslintrc.${myapp}.json`);
rootEslint = readJson('.eslintrc.json');
e2eEslint = readJson('e2e/.eslintrc.json');
const libEslint = readJson(`libs/${mylib}/.eslintrc.json`);

// should directly refer to nx plugin only in the root
expect(rootEslint.plugins).toEqual(['@nrwl/nx']);
expect(appEslint.plugins).toBeUndefined();
expect(e2eEslint.plugins).toBeUndefined();
// should extend framework plugin and root config
expect(appEslint.extends).toEqual([
'plugin:@nrwl/nx/react',
'./.eslintrc.json',
]);
expect(e2eEslint.extends).toEqual([
'plugin:cypress/recommended',
'../.eslintrc.json',
]);
expect(libEslint.extends).toEqual([
'plugin:@nrwl/nx/react',
'../../.eslintrc.json',
]);
// should have no plugin extends
expect(appEslint.overrides[0].extends).toBeUndefined();
expect(appEslint.overrides[1].extends).toBeUndefined();
expect(e2eEslint.overrides[0].extends).toBeUndefined();
expect(e2eEslint.overrides[1].extends).toBeUndefined();
expect(libEslint.overrides[1].extends).toBeUndefined();
expect(libEslint.overrides[1].extends).toBeUndefined();
});
});
});

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import {
import { Linter, lintProjectGenerator } from '@nrwl/linter';
import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial';
import { getRelativePathToRootTsConfig } from '@nrwl/workspace/src/utilities/typescript';
import {
globalJavaScriptOverrides,
globalTypeScriptOverrides,
} from '@nrwl/linter/src/generators/init/global-eslint-config';

import { join } from 'path';
import { installedCypressVersion } from '../../utils/cypress-version';
Expand Down Expand Up @@ -177,6 +181,7 @@ export async function addLinter(host: Tree, options: CypressProjectSchema) {
],
setParserOptionsProject: options.setParserOptionsProject,
skipPackageJson: options.skipPackageJson,
rootProject: options.rootProject,
});

if (!options.linter || options.linter !== Linter.EsLint) {
Expand All @@ -192,8 +197,16 @@ export async function addLinter(host: Tree, options: CypressProjectSchema) {
: () => {};

updateJson(host, join(options.projectRoot, '.eslintrc.json'), (json) => {
json.extends = ['plugin:cypress/recommended', ...json.extends];
if (options.rootProject) {
json.plugins = ['@nrwl/nx'];
json.extends = ['plugin:cypress/recommended'];
} else {
json.extends = ['plugin:cypress/recommended', ...json.extends];
}
json.overrides = [
...(options.rootProject
? [globalTypeScriptOverrides, globalJavaScriptOverrides]
: []),
/**
* In order to ensure maximum efficiency when typescript-eslint generates TypeScript Programs
* behind the scenes during lint runs, we need to make sure the project is configured to use its
Expand Down
3 changes: 2 additions & 1 deletion packages/linter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"builders": "./executors.json",
"schematics": "./generators.json",
"peerDependencies": {
"eslint": "^8.0.0"
"eslint": "^8.0.0",
"js-yaml": "4.1.0"

This comment has been minimized.

Copy link
@FrozenPandaz

FrozenPandaz Nov 25, 2022

Collaborator

Why is this necessary?

},
"dependencies": {
"@nrwl/devkit": "file:../devkit",
Expand Down
80 changes: 80 additions & 0 deletions packages/linter/src/generators/init/global-eslint-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { ESLint, Linter as LinterType } from 'eslint';

/**
* This configuration is intended to apply to all TypeScript source files.
* See the eslint-plugin-nx package for what is in the referenced shareable config.
*/
export const globalTypeScriptOverrides = {
files: ['*.ts', '*.tsx'],
extends: ['plugin:@nrwl/nx/typescript'],
/**
* Having an empty rules object present makes it more obvious to the user where they would
* extend things from if they needed to
*/
rules: {},
};

/**
* This configuration is intended to apply to all JavaScript source files.
* See the eslint-plugin-nx package for what is in the referenced shareable config.
*/
export const globalJavaScriptOverrides = {
files: ['*.js', '*.jsx'],
extends: ['plugin:@nrwl/nx/javascript'],
/**
* Having an empty rules object present makes it more obvious to the user where they would
* extend things from if they needed to
*/
rules: {},
};

/**
* This configuration is intended to apply to all "source code" (but not
* markup like HTML, or other custom file types like GraphQL)
*/
export const moduleBoundariesOverride = {
files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
rules: {
'@nrwl/nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
allow: [],
depConstraints: [{ sourceTag: '*', onlyDependOnLibsWithTags: ['*'] }],
},
],
} as LinterType.RulesRecord,
};

export const getGlobalEsLintConfiguration = (
unitTestRunner?: string,
rootProject?: boolean
) => {
const config: ESLint.ConfigData = {
root: true,
ignorePatterns: rootProject ? ['!**/*'] : ['**/*'],
plugins: ['@nrwl/nx'],
/**
* We leverage ESLint's "overrides" capability so that we can set up a root config which will support
* all permutations of Nx workspaces across all frameworks, libraries and tools.
*
* The key point is that we need entirely different ESLint config to apply to different types of files,
* but we still want to share common config where possible.
*/
overrides: [
...(rootProject ? [] : [moduleBoundariesOverride]),
globalTypeScriptOverrides,
globalJavaScriptOverrides,
],
};
if (unitTestRunner === 'jest') {
config.overrides.push({
files: ['*.spec.ts', '*.spec.tsx', '*.spec.js', '*.spec.jsx'],
env: {
jest: true,
},
rules: {},
});
}
return config;
};
116 changes: 116 additions & 0 deletions packages/linter/src/generators/init/init-migration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import {
joinPathFragments,
offsetFromRoot,
ProjectConfiguration,
TargetConfiguration,
Tree,
updateJson,
updateProjectConfiguration,
writeJson,
} from '@nrwl/devkit';
import { basename, dirname } from 'path';
import { findEslintFile } from '../utils/eslint-file';
import { getGlobalEsLintConfiguration } from './global-eslint-config';

const FILE_EXTENSION_REGEX = /(?<!(^|\/))(\.[^/.]+)$/;

export function migrateConfigToMonorepoStyle(
projects: ProjectConfiguration[],
tree: Tree,
unitTestRunner: string
): void {
// copy the root's .eslintrc.json to new name
const rootProject = projects.find((p) => p.root === '.');
const eslintPath =
rootProject.targets?.lint?.options?.eslintConfig || findEslintFile(tree);
const pathSegments = eslintPath.split(FILE_EXTENSION_REGEX).filter(Boolean);
const rootProjEslintPath =
pathSegments.length > 1
? pathSegments.join(`.${rootProject.name}`)
: `.${rootProject.name}.${rootProject.name}`;
tree.write(rootProjEslintPath, tree.read(eslintPath));

// update root project's configuration
const lintTarget = findLintTarget(rootProject);
lintTarget.options.eslintConfig = rootProjEslintPath;
updateProjectConfiguration(tree, rootProject.name, rootProject);

// replace root eslint with default global
tree.delete(eslintPath);
writeJson(
tree,
'.eslintrc.json',
getGlobalEsLintConfiguration(unitTestRunner)
);

// update extens in all projects' eslint configs
projects.forEach((project) => {
const lintTarget = findLintTarget(project);
if (lintTarget) {
const projectEslintPath = joinPathFragments(
project.root,
lintTarget.options.eslintConfig || findEslintFile(tree, project.root)
);
migrateEslintFile(projectEslintPath, tree);
}
});
}

export function findLintTarget(
project: ProjectConfiguration
): TargetConfiguration {
return Object.entries(project.targets).find(
([name, target]) =>
name === 'lint' || target.executor === '@nrwl/linter:eslint'
)?.[1];
}

function migrateEslintFile(projectEslintPath: string, tree: Tree) {
if (projectEslintPath.endsWith('.json')) {
updateJson(tree, projectEslintPath, (json) => {
// we have a new root now
delete json.root;
// remove nrwl/nx plugins
if (json.plugins) {
json.plugins = json.plugins.filter((p) => p !== '@nrwl/nx');
if (json.plugins.length === 0) {
delete json.plugins;
}
}
// add extends
json.extends = json.extends || [];
const pathToRootConfig = `${offsetFromRoot(
dirname(projectEslintPath)
)}.eslintrc.json`;
if (json.extends.indexOf(pathToRootConfig) === -1) {
json.extends.push(pathToRootConfig);
}
// cleanup overrides
if (json.overrides) {
json.overrides.forEach((override) => {
if (override.extends) {
override.extends = override.extends.filter(
(ext) =>
ext !== 'plugin:@nrwl/nx/typescript' &&
ext !== 'plugin:@nrwl/nx/javascript'
);
if (override.extends.length === 0) {
delete override.extends;
}
}
});
}
return json;
});
return;
}
if (
projectEslintPath.endsWith('.yml') ||
projectEslintPath.endsWith('.yaml')
) {
console.warn('YAML eslint config is not supported yet for migration');
}
if (projectEslintPath.endsWith('.js') || projectEslintPath.endsWith('.cjs')) {
console.warn('YAML eslint config is not supported yet for migration');
}
}
Loading

1 comment on commit 110b5f2

@vercel
Copy link

@vercel vercel bot commented on 110b5f2 Nov 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

nx-dev – ./

nx-dev-nrwl.vercel.app
nx.dev
nx-five.vercel.app
nx-dev-git-master-nrwl.vercel.app

Please sign in to comment.