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": {
"init-workspace-rules": {
"factory": "./src/generators/init-workspace-rules/init-workspace-rules#lintInitWorkspaceRulesSchematic",
"schema": "./src/generators/init-workspace-rules/schema.json",
"description": "Initialize workspace ESLint rules",
"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": {
"init-workspace-rules": {
"factory": "./src/generators/init-workspace-rules/init-workspace-rules#lintInitWorkspaceRulesGenerator",
"schema": "./src/generators/init-workspace-rules/schema.json",
"description": "Initialize workspace ESLint rules",
"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
}
}
}
1 change: 1 addition & 0 deletions packages/linter/package.json
Expand Up @@ -27,6 +27,7 @@
"migrations": "./migrations.json"
},
"builders": "./executors.json",
"schematics": "./generators.json",
"dependencies": {
"@nrwl/devkit": "*",
"glob": "7.1.4",
Expand Down
@@ -0,0 +1,45 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`@nrwl/linter:init-workspace-rules should generate the required files 1`] = `
"/**
* Import your custom workspace rules at the top of this file.
*
* For example:
*
* import {
* RULE_NAME as myCustomRuleName,
* rule as myCustomRule
* } from './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: {}
};
"
`;

exports[`@nrwl/linter:init-workspace-rules should generate the required files 2`] = `
"{
\\"extends\\": \\"../../tsconfig.base.json\\",
\\"compilerOptions\\": {
\\"module\\": \\"commonjs\\"
}
}
"
`;
@@ -0,0 +1,30 @@
/**
* Import your custom workspace rules at the top of this file.
*
* For example:
*
* import {
* RULE_NAME as myCustomRuleName,
* rule as myCustomRule
* } from './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: {}
};
@@ -0,0 +1,6 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs"
}
}
@@ -0,0 +1,52 @@
import { readJson, Tree } from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { lintInitWorkspaceRulesGenerator } from './init-workspace-rules';

describe('@nrwl/linter:init-workspace-rules', () => {
let tree: Tree;

beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});

it('should update implicitDependencies in nx.json', async () => {
expect(
readJson(tree, 'nx.json').implicitDependencies
).toMatchInlineSnapshot(`undefined`);

await lintInitWorkspaceRulesGenerator(tree);

expect(readJson(tree, 'nx.json').implicitDependencies)
.toMatchInlineSnapshot(`
Object {
"tools/eslint-rules/**/*": "*",
}
`);
});

it('should generate the required files', async () => {
await lintInitWorkspaceRulesGenerator(tree);

expect(tree.read('tools/eslint-rules/index.ts', 'utf-8')).toMatchSnapshot();
expect(
tree.read('tools/eslint-rules/tsconfig.json', 'utf-8')
).toMatchSnapshot();
});

it('should not update the required files if they already exist', async () => {
const customIndexContents = `custom index contents`;
tree.write('tools/eslint-rules/index.ts', customIndexContents);

const customTsconfigContents = `custom tsconfig contents`;
tree.write('tools/eslint-rules/tsconfig.json', customTsconfigContents);

await lintInitWorkspaceRulesGenerator(tree);

expect(tree.read('tools/eslint-rules/index.ts', 'utf-8')).toEqual(
customIndexContents
);
expect(tree.read('tools/eslint-rules/tsconfig.json', 'utf-8')).toEqual(
customTsconfigContents
);
});
});
@@ -0,0 +1,33 @@
import {
convertNxGenerator,
generateFiles,
joinPathFragments,
readWorkspaceConfiguration,
Tree,
updateWorkspaceConfiguration,
} from '@nrwl/devkit';
import { join } from 'path';
import { workspaceLintPluginDir } from '../../utils/workspace-lint-rules';

export async function lintInitWorkspaceRulesGenerator(tree: Tree) {
// Generate the required files if they don't exist yet
if (!tree.exists(joinPathFragments(workspaceLintPluginDir, 'index.ts'))) {
generateFiles(tree, join(__dirname, 'files'), workspaceLintPluginDir, {
tmpl: '',
});
}

// Ensure that when workspace rules are updated they cause all projects to be affected
const workspaceConfig = readWorkspaceConfiguration(tree);
updateWorkspaceConfiguration(tree, {
...workspaceConfig,
implicitDependencies: {
...workspaceConfig.implicitDependencies,
'tools/eslint-rules/**/*': '*',
},
});
}

export const lintInitWorkspaceRulesSchematic = convertNxGenerator(
lintInitWorkspaceRulesGenerator
);
16 changes: 16 additions & 0 deletions packages/linter/src/generators/init-workspace-rules/schema.json
@@ -0,0 +1,16 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "NxInitWorkspaceLintRules",
"cli": "nx",
"title": "Init Workspace Lint Rules",
"description": "Initialize Workspace Lint Rules",
"type": "object",
"examples": [
{
"command": "g @nrwl/linter:init-workspace-rules",
"description": "Initialize Workspace Lint Rules"
}
],
"properties": {},
"required": []
}