Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(linter): add support for workspace rules #6859

Merged
merged 11 commits into from Sep 1, 2021
Merged
1 change: 1 addition & 0 deletions docs/angular/generators.json
Expand Up @@ -5,6 +5,7 @@
"express",
"gatsby",
"jest",
"linter",
"nest",
"next",
"node",
Expand Down
1 change: 1 addition & 0 deletions docs/node/generators.json
Expand Up @@ -5,6 +5,7 @@
"express",
"gatsby",
"jest",
"linter",
"nest",
"next",
"node",
Expand Down
1 change: 1 addition & 0 deletions docs/react/generators.json
Expand Up @@ -5,6 +5,7 @@
"express",
"gatsby",
"jest",
"linter",
"nest",
"next",
"node",
Expand Down
4 changes: 3 additions & 1 deletion packages/eslint-plugin-nx/package.json
Expand Up @@ -35,6 +35,8 @@
"@nrwl/devkit": "*",
"@nrwl/workspace": "*",
"@typescript-eslint/experimental-utils": "~4.28.3",
"confusing-browser-globals": "^1.0.9"
"confusing-browser-globals": "^1.0.9",
"ts-node": "^9.1.1",
"tsconfig-paths": "^3.9.0"
}
}
16 changes: 16 additions & 0 deletions packages/eslint-plugin-nx/src/constants.ts
@@ -0,0 +1,16 @@
import { appRootPath } from '@nrwl/tao/src/utils/app-root';
import { join } from 'path';

export const WORKSPACE_PLUGIN_DIR = join(appRootPath, 'tools/eslint-rules');

/**
* We add a namespace so that we mitigate the risk of rule name collisions as much as
* possible between what users might create in their workspaces and what we might want
* to offer directly in eslint-plugin-nx in the future.
*
* E.g. if a user writes a rule called "foo", then they will include it in their ESLint
* config files as:
*
* "@nrwl/nx/workspace/foo": "error"
*/
export const WORKSPACE_RULE_NAMESPACE = 'workspace';
4 changes: 4 additions & 0 deletions packages/eslint-plugin-nx/src/index.ts
Expand Up @@ -11,6 +11,9 @@ import enforceModuleBoundaries, {
RULE_NAME as enforceModuleBoundariesRuleName,
} from './rules/enforce-module-boundaries';

// Resolve any custom rules that might exist in the current workspace
import { workspaceRules } from './resolve-workspace-rules';

module.exports = {
configs: {
typescript,
Expand All @@ -24,5 +27,6 @@ module.exports = {
},
rules: {
[enforceModuleBoundariesRuleName]: enforceModuleBoundaries,
...workspaceRules,
},
};
52 changes: 52 additions & 0 deletions packages/eslint-plugin-nx/src/resolve-workspace-rules.ts
@@ -0,0 +1,52 @@
import type { TSESLint } from '@typescript-eslint/experimental-utils';
import { WORKSPACE_PLUGIN_DIR, WORKSPACE_RULE_NAMESPACE } from './constants';

type ESLintRules = Record<string, TSESLint.RuleModule<string, unknown[]>>;

/**
* Optionally, if ts-node and tsconfig-paths are available in the current workspace, apply the require
* register hooks so that .ts files can be used for writing workspace lint rules.
*
* If ts-node and tsconfig-paths are not available, the user can still provide an index.js file in
* tools/eslint-rules and write their rules in JavaScript and the fundamentals will still work (but
* workspace path mapping will not, for example).
*/
try {
require('ts-node').register({
dir: WORKSPACE_PLUGIN_DIR,
});

const tsconfigPaths = require('tsconfig-paths');

// Load the tsconfig from tools/eslint-rules/tsconfig.json
const tsConfigResult = tsconfigPaths.loadConfig(WORKSPACE_PLUGIN_DIR);

/**
* Register the custom workspace path mappings with node so that workspace libraries
* can be imported and used within custom workspace lint rules.
*/
tsconfigPaths.register({
baseUrl: tsConfigResult.absoluteBaseUrl,
paths: tsConfigResult.paths,
});
} catch (err) {}

export const workspaceRules = ((): ESLintRules => {
try {
/**
* Currently we only support applying the rules from the user's workspace plugin object
* (i.e. not other things that plugings can expose like configs, processors etc)
*/
const { rules } = require(WORKSPACE_PLUGIN_DIR);
const localWorkspaceRules: ESLintRules = rules;

// Apply the namespace to the resolved rules
const namespacedRules: ESLintRules = {};
for (const [ruleName, ruleConfig] of Object.entries(localWorkspaceRules)) {
namespacedRules[`${WORKSPACE_RULE_NAMESPACE}/${ruleName}`] = ruleConfig;
}
return namespacedRules;
} catch (err) {
return {};
}
})();
32 changes: 32 additions & 0 deletions packages/linter/generators.json
@@ -0,0 +1,32 @@
{
"name": "nx/linter",
"version": "0.1",
"schematics": {
"workspace-rules-project": {
"factory": "./src/generators/workspace-rules-project/workspace-rules-project#lintWorkspaceRulesProjectSchematic",
"schema": "./src/generators/workspace-rules-project/schema.json",
"description": "Create the Workspace Lint Rules Project",
"hidden": true
},
"workspace-rule": {
"factory": "./src/generators/workspace-rule/workspace-rule#lintWorkspaceRuleSchematic",
"schema": "./src/generators/workspace-rule/schema.json",
"description": "Create a new workspace ESLint rule",
"hidden": true
}
},
"generators": {
"workspace-rules-project": {
"factory": "./src/generators/workspace-rules-project/workspace-rules-project#lintWorkspaceRulesProjectGenerator",
"schema": "./src/generators/workspace-rules-project/schema.json",
"description": "Create the Workspace Lint Rules Project",
"hidden": true
},
"workspace-rule": {
"factory": "./src/generators/workspace-rule/workspace-rule#lintWorkspaceRuleGenerator",
"schema": "./src/generators/workspace-rule/schema.json",
"description": "Create a new workspace ESLint rule",
"hidden": true
}
}
}
2 changes: 2 additions & 0 deletions packages/linter/package.json
Expand Up @@ -27,8 +27,10 @@
"migrations": "./migrations.json"
},
"builders": "./executors.json",
"schematics": "./generators.json",
"dependencies": {
"@nrwl/devkit": "*",
"@nrwl/jest": "*",
"glob": "7.1.4",
"minimatch": "3.0.4",
"tmp": "~0.2.1",
Expand Down
@@ -0,0 +1,205 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`@nrwl/linter:workspace-rule --dir should support creating the rule in a nested directory 1`] = `
"/**
* This file sets you up with with structure needed for an ESLint rule.
*
* It leverages utilities from @typescript-eslint to allow TypeScript to
* provide autocompletions etc for the configuration.
*
* Your rule's custom logic will live within the create() method below
* and you can learn more about writing ESLint rules on the official guide:
*
* https://eslint.org/docs/developer-guide/working-with-rules
*
* You can also view many examples of existing rules here:
*
* https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin/src/rules
*/

import { ESLintUtils } from '@typescript-eslint/experimental-utils';

// NOTE: The rule will be available in ESLint configs as \\"@nrwl/nx/workspace/another-rule\\"
export const RULE_NAME = 'another-rule';

export const rule = ESLintUtils.RuleCreator(() => __filename)({
name: RULE_NAME,
meta: {
type: 'problem',
docs: {
description: \`\`,
category: 'Possible Errors',
recommended: 'error',
},
schema: [],
messages: {},
},
defaultOptions: [],
create(context) {
return {};
},
});
"
`;

exports[`@nrwl/linter:workspace-rule --dir should support creating the rule in a nested directory 2`] = `
"import { TSESLint } from '@typescript-eslint/experimental-utils';
import { rule, RULE_NAME } from './another-rule';

const ruleTester = new TSESLint.RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
});

ruleTester.run(RULE_NAME, rule, {
valid: [\`const example = true;\`],
invalid: [],
});
"
`;

exports[`@nrwl/linter:workspace-rule --dir should support creating the rule in a nested directory with multiple levels of nesting 1`] = `
"/**
* This file sets you up with with structure needed for an ESLint rule.
*
* It leverages utilities from @typescript-eslint to allow TypeScript to
* provide autocompletions etc for the configuration.
*
* Your rule's custom logic will live within the create() method below
* and you can learn more about writing ESLint rules on the official guide:
*
* https://eslint.org/docs/developer-guide/working-with-rules
*
* You can also view many examples of existing rules here:
*
* https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin/src/rules
*/

import { ESLintUtils } from '@typescript-eslint/experimental-utils';

// NOTE: The rule will be available in ESLint configs as \\"@nrwl/nx/workspace/one-more-rule\\"
export const RULE_NAME = 'one-more-rule';

export const rule = ESLintUtils.RuleCreator(() => __filename)({
name: RULE_NAME,
meta: {
type: 'problem',
docs: {
description: \`\`,
category: 'Possible Errors',
recommended: 'error',
},
schema: [],
messages: {},
},
defaultOptions: [],
create(context) {
return {};
},
});
"
`;

exports[`@nrwl/linter:workspace-rule --dir should support creating the rule in a nested directory with multiple levels of nesting 2`] = `
"import { TSESLint } from '@typescript-eslint/experimental-utils';
import { rule, RULE_NAME } from './one-more-rule';

const ruleTester = new TSESLint.RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
});

ruleTester.run(RULE_NAME, rule, {
valid: [\`const example = true;\`],
invalid: [],
});
"
`;

exports[`@nrwl/linter:workspace-rule should generate the required files 1`] = `
"/**
* This file sets you up with with structure needed for an ESLint rule.
*
* It leverages utilities from @typescript-eslint to allow TypeScript to
* provide autocompletions etc for the configuration.
*
* Your rule's custom logic will live within the create() method below
* and you can learn more about writing ESLint rules on the official guide:
*
* https://eslint.org/docs/developer-guide/working-with-rules
*
* You can also view many examples of existing rules here:
*
* https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin/src/rules
*/

import { ESLintUtils } from '@typescript-eslint/experimental-utils';

// NOTE: The rule will be available in ESLint configs as \\"@nrwl/nx/workspace/my-rule\\"
export const RULE_NAME = 'my-rule';

export const rule = ESLintUtils.RuleCreator(() => __filename)({
name: RULE_NAME,
meta: {
type: 'problem',
docs: {
description: \`\`,
category: 'Possible Errors',
recommended: 'error',
},
schema: [],
messages: {},
},
defaultOptions: [],
create(context) {
return {};
},
});
"
`;

exports[`@nrwl/linter:workspace-rule should generate the required files 2`] = `
"import { TSESLint } from '@typescript-eslint/experimental-utils';
import { rule, RULE_NAME } from './my-rule';

const ruleTester = new TSESLint.RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
});

ruleTester.run(RULE_NAME, rule, {
valid: [\`const example = true;\`],
invalid: [],
});
"
`;

exports[`@nrwl/linter:workspace-rule should update the plugin index.ts with the new rule 1`] = `
"import { RULE_NAME as myRuleName, rule as myRule } from './rules/my-rule';
/**
* Import your custom workspace rules at the top of this file.
*
* For example:
*
* import { RULE_NAME as myCustomRuleName, rule as myCustomRule } from './rules/my-custom-rule';
*
* In order to quickly get started with writing rules you can use the
* following generator command and provide your desired rule name:
*
* \`\`\`sh
* npx nx g @nrwl/linter:workspace-rule {{ NEW_RULE_NAME }}
* \`\`\`
*/

module.exports = {
/**
* Apply the imported custom rules here.
*
* For example (using the example import above):
*
* rules: {
* [myCustomRuleName]: myCustomRule
* }
*/
rules: {[myRuleName]: myRule
}
};
"
`;
@@ -0,0 +1,11 @@
import { TSESLint } from '@typescript-eslint/experimental-utils';
import { rule, RULE_NAME } from './<%= name %>';

const ruleTester = new TSESLint.RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
});

ruleTester.run(RULE_NAME, rule, {
valid: [`const example = true;`],
invalid: [],
});