From bafabb3a11c1d9084e114569f700cd793aac199f Mon Sep 17 00:00:00 2001 From: James Henry Date: Wed, 1 Sep 2021 20:49:11 +0400 Subject: [PATCH] feat(linter): add support for workspace rules (#6859) * feat(linter): add support for workspace rules * chore(linter): move deps to correct package * feat(linter): workspace-lint-rules as project with test target * chore(linter): tiny clean up * fix(linter): update generators.json after refactor * chore(linter): implement e2e test and PR feedback * docs(linter): update generator docs * docs(linter): update generator docs * docs(linter): update generator docs --- .../api-linter/generators/workspace-rule.md | 55 +++++ docs/angular/generators.json | 1 + docs/map.json | 15 ++ .../api-linter/generators/workspace-rule.md | 55 +++++ docs/node/generators.json | 1 + .../api-linter/generators/workspace-rule.md | 55 +++++ docs/react/generators.json | 1 + e2e/linter/src/linter.test.ts | 189 ++++++++++++++++ packages/eslint-plugin-nx/package.json | 4 +- packages/eslint-plugin-nx/src/constants.ts | 16 ++ packages/eslint-plugin-nx/src/index.ts | 4 + .../src/resolve-workspace-rules.ts | 52 +++++ packages/linter/generators.json | 30 +++ packages/linter/package.json | 2 + .../__snapshots__/workspace-rule.spec.ts.snap | 205 ++++++++++++++++++ .../files/__name__.spec.ts__tmpl__ | 11 + .../workspace-rule/files/__name__.ts__tmpl__ | 38 ++++ .../src/generators/workspace-rule/schema.json | 35 +++ .../workspace-rule/workspace-rule.spec.ts | 185 ++++++++++++++++ .../workspace-rule/workspace-rule.ts | 120 ++++++++++ .../workspace-rules-project.spec.ts.snap | 99 +++++++++ .../files/index.ts__tmpl__ | 27 +++ .../files/tsconfig.json__tmpl__ | 13 ++ .../files/tsconfig.lint.json__tmpl__ | 9 + .../workspace-rules-project/schema.json | 16 ++ .../workspace-rules-project.spec.ts | 105 +++++++++ .../workspace-rules-project.ts | 64 ++++++ .../linter/src/utils/workspace-lint-rules.ts | 3 + 28 files changed, 1409 insertions(+), 1 deletion(-) create mode 100644 docs/angular/api-linter/generators/workspace-rule.md create mode 100644 docs/node/api-linter/generators/workspace-rule.md create mode 100644 docs/react/api-linter/generators/workspace-rule.md create mode 100644 packages/eslint-plugin-nx/src/constants.ts create mode 100644 packages/eslint-plugin-nx/src/resolve-workspace-rules.ts create mode 100644 packages/linter/generators.json create mode 100644 packages/linter/src/generators/workspace-rule/__snapshots__/workspace-rule.spec.ts.snap create mode 100644 packages/linter/src/generators/workspace-rule/files/__name__.spec.ts__tmpl__ create mode 100644 packages/linter/src/generators/workspace-rule/files/__name__.ts__tmpl__ create mode 100644 packages/linter/src/generators/workspace-rule/schema.json create mode 100644 packages/linter/src/generators/workspace-rule/workspace-rule.spec.ts create mode 100644 packages/linter/src/generators/workspace-rule/workspace-rule.ts create mode 100644 packages/linter/src/generators/workspace-rules-project/__snapshots__/workspace-rules-project.spec.ts.snap create mode 100644 packages/linter/src/generators/workspace-rules-project/files/index.ts__tmpl__ create mode 100644 packages/linter/src/generators/workspace-rules-project/files/tsconfig.json__tmpl__ create mode 100644 packages/linter/src/generators/workspace-rules-project/files/tsconfig.lint.json__tmpl__ create mode 100644 packages/linter/src/generators/workspace-rules-project/schema.json create mode 100644 packages/linter/src/generators/workspace-rules-project/workspace-rules-project.spec.ts create mode 100644 packages/linter/src/generators/workspace-rules-project/workspace-rules-project.ts create mode 100644 packages/linter/src/utils/workspace-lint-rules.ts diff --git a/docs/angular/api-linter/generators/workspace-rule.md b/docs/angular/api-linter/generators/workspace-rule.md new file mode 100644 index 0000000000000..64b1fb49c8a5a --- /dev/null +++ b/docs/angular/api-linter/generators/workspace-rule.md @@ -0,0 +1,55 @@ +# @nrwl/linter:workspace-rule + +Create a new workspace ESLint rule + +## Usage + +```bash +nx generate workspace-rule ... +``` + +By default, Nx will search for `workspace-rule` in the default collection provisioned in `angular.json`. + +You can specify the collection explicitly as follows: + +```bash +nx g @nrwl/linter:workspace-rule ... +``` + +Show what will be generated without writing to disk: + +```bash +nx g workspace-rule ... --dry-run +``` + +### Examples + +Create a new workspace lint rule called my-custom-rule: + +```bash +nx g @nrwl/linter:workspace-rule my-custom-rule +``` + +Create a new workspace lint rule located at tools/eslint-rules/a/b/c/my-custom-rule.ts: + +```bash +nx g @nrwl/linter:workspace-rule --name=my-custom-rule --directory=a/b/c +``` + +## Options + +### directory (_**required**_) + +Alias(es): dir + +Default: `rules` + +Type: `string` + +Create the rule under this directory within tools/eslint-rules/ (can be nested). + +### name (_**required**_) + +Type: `string` + +The name of the new rule diff --git a/docs/angular/generators.json b/docs/angular/generators.json index 0ae1fbf35a4b1..c2f47c1b3291e 100644 --- a/docs/angular/generators.json +++ b/docs/angular/generators.json @@ -5,6 +5,7 @@ "express", "gatsby", "jest", + "linter", "nest", "next", "node", diff --git a/docs/map.json b/docs/map.json index bd3e1f7314737..860a337baa8e2 100644 --- a/docs/map.json +++ b/docs/map.json @@ -670,6 +670,11 @@ "name": "lint executor", "id": "lint", "file": "angular/api-linter/executors/lint" + }, + { + "name": "workspace-rule generator", + "id": "workspace-rule", + "file": "angular/api-linter/generators/workspace-rule" } ] }, @@ -1959,6 +1964,11 @@ "name": "lint executor", "id": "lint", "file": "react/api-linter/executors/lint" + }, + { + "name": "workspace-rule generator", + "id": "workspace-rule", + "file": "react/api-linter/generators/workspace-rule" } ] }, @@ -3210,6 +3220,11 @@ "name": "lint executor", "id": "lint", "file": "node/api-linter/executors/lint" + }, + { + "name": "workspace-rule generator", + "id": "workspace-rule", + "file": "node/api-linter/generators/workspace-rule" } ] }, diff --git a/docs/node/api-linter/generators/workspace-rule.md b/docs/node/api-linter/generators/workspace-rule.md new file mode 100644 index 0000000000000..b077b41106b37 --- /dev/null +++ b/docs/node/api-linter/generators/workspace-rule.md @@ -0,0 +1,55 @@ +# @nrwl/linter:workspace-rule + +Create a new workspace ESLint rule + +## Usage + +```bash +nx generate workspace-rule ... +``` + +By default, Nx will search for `workspace-rule` in the default collection provisioned in `workspace.json`. + +You can specify the collection explicitly as follows: + +```bash +nx g @nrwl/linter:workspace-rule ... +``` + +Show what will be generated without writing to disk: + +```bash +nx g workspace-rule ... --dry-run +``` + +### Examples + +Create a new workspace lint rule called my-custom-rule: + +```bash +nx g @nrwl/linter:workspace-rule my-custom-rule +``` + +Create a new workspace lint rule located at tools/eslint-rules/a/b/c/my-custom-rule.ts: + +```bash +nx g @nrwl/linter:workspace-rule --name=my-custom-rule --directory=a/b/c +``` + +## Options + +### directory (_**required**_) + +Alias(es): dir + +Default: `rules` + +Type: `string` + +Create the rule under this directory within tools/eslint-rules/ (can be nested). + +### name (_**required**_) + +Type: `string` + +The name of the new rule diff --git a/docs/node/generators.json b/docs/node/generators.json index 0ae1fbf35a4b1..c2f47c1b3291e 100644 --- a/docs/node/generators.json +++ b/docs/node/generators.json @@ -5,6 +5,7 @@ "express", "gatsby", "jest", + "linter", "nest", "next", "node", diff --git a/docs/react/api-linter/generators/workspace-rule.md b/docs/react/api-linter/generators/workspace-rule.md new file mode 100644 index 0000000000000..b077b41106b37 --- /dev/null +++ b/docs/react/api-linter/generators/workspace-rule.md @@ -0,0 +1,55 @@ +# @nrwl/linter:workspace-rule + +Create a new workspace ESLint rule + +## Usage + +```bash +nx generate workspace-rule ... +``` + +By default, Nx will search for `workspace-rule` in the default collection provisioned in `workspace.json`. + +You can specify the collection explicitly as follows: + +```bash +nx g @nrwl/linter:workspace-rule ... +``` + +Show what will be generated without writing to disk: + +```bash +nx g workspace-rule ... --dry-run +``` + +### Examples + +Create a new workspace lint rule called my-custom-rule: + +```bash +nx g @nrwl/linter:workspace-rule my-custom-rule +``` + +Create a new workspace lint rule located at tools/eslint-rules/a/b/c/my-custom-rule.ts: + +```bash +nx g @nrwl/linter:workspace-rule --name=my-custom-rule --directory=a/b/c +``` + +## Options + +### directory (_**required**_) + +Alias(es): dir + +Default: `rules` + +Type: `string` + +Create the rule under this directory within tools/eslint-rules/ (can be nested). + +### name (_**required**_) + +Type: `string` + +The name of the new rule diff --git a/docs/react/generators.json b/docs/react/generators.json index 0ae1fbf35a4b1..c2f47c1b3291e 100644 --- a/docs/react/generators.json +++ b/docs/react/generators.json @@ -5,6 +5,7 @@ "express", "gatsby", "jest", + "linter", "nest", "next", "node", diff --git a/e2e/linter/src/linter.test.ts b/e2e/linter/src/linter.test.ts index 500112e9541a9..e2123c694eb0d 100644 --- a/e2e/linter/src/linter.test.ts +++ b/e2e/linter/src/linter.test.ts @@ -9,6 +9,7 @@ import { uniq, updateFile, } from '@nrwl/e2e/utils'; +import * as ts from 'typescript'; describe('Linter', () => { it('linting should error when rules are not followed', () => { @@ -213,4 +214,192 @@ describe('Linter', () => { }); expect(() => checkFilesExist(outputFile)).not.toThrow(); }, 1000000); + + describe('workspace lint rules', () => { + it('should supporting creating, testing and using workspace lint rules', () => { + newProject({ name: uniq('myproj') }); + const myapp = uniq('myapp'); + runCLI(`generate @nrwl/react:app ${myapp}`); + + // Generate a new rule (should also scaffold the required workspace project and tests) + const newRuleName = 'e2e-test-rule-name'; + runCLI(`generate @nrwl/linter:workspace-rule ${newRuleName}`); + + // Ensure that the unit tests for the new rule are runnable + const unitTestsOutput = runCLI(`test eslint-rules`); + expect(unitTestsOutput).toContain('Running target "test" succeeded'); + + // Update the rule for the e2e test so that we can assert that it produces the expected lint failure when used + const knownLintErrorMessage = 'e2e test known error message'; + const newRulePath = `tools/eslint-rules/rules/${newRuleName}.ts`; + const newRuleGeneratedContents = readFile(newRulePath); + const updatedRuleContents = updateGeneratedRuleImplementation( + newRulePath, + newRuleGeneratedContents, + knownLintErrorMessage + ); + updateFile(newRulePath, updatedRuleContents); + + const newRuleNameForUsage = `@nrwl/nx/workspace/${newRuleName}`; + + // Add the new workspace rule to the lint config and run linting + const eslintrc = readJson('.eslintrc.json'); + eslintrc.overrides.forEach((override) => { + if (override.files.includes('*.ts')) { + override.rules[newRuleNameForUsage] = 'error'; + } + }); + updateFile('.eslintrc.json', JSON.stringify(eslintrc, null, 2)); + + const lintOutput = runCLI(`lint ${myapp}`, { silenceError: true }); + expect(lintOutput).toContain(newRuleNameForUsage); + expect(lintOutput).toContain(knownLintErrorMessage); + }, 1000000); + }); }); + +/** + * Update the generated rule implementation to produce a known lint error from all files. + * + * It is important that we do this surgically via AST transformations, otherwise we will + * drift further and further away from the original generated code and therefore make our + * e2e test less accurate and less valuable. + */ +function updateGeneratedRuleImplementation( + newRulePath: string, + newRuleGeneratedContents: string, + knownLintErrorMessage: string +): string { + const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + const newRuleSourceFile = ts.createSourceFile( + newRulePath, + newRuleGeneratedContents, + ts.ScriptTarget.Latest, + true + ); + + const messageId = 'e2eMessageId'; + + const transformer = + (context: ts.TransformationContext) => + (rootNode: T) => { + function visit(node: ts.Node): ts.Node { + /** + * Add an ESLint messageId which will show the knownLintErrorMessage + * + * i.e. + * + * messages: { + * e2eMessageId: knownLintErrorMessage + * } + */ + if ( + ts.isPropertyAssignment(node) && + ts.isIdentifier(node.name) && + node.name.escapedText === 'messages' + ) { + return ts.factory.updatePropertyAssignment( + node, + node.name, + ts.factory.createObjectLiteralExpression([ + ts.factory.createPropertyAssignment( + messageId, + ts.factory.createStringLiteral(knownLintErrorMessage) + ), + ]) + ); + } + + /** + * Update the rule implementation to report the knownLintErrorMessage on every Program node + * + * i.e. + * + * create(context) { + * return { + * Program(node) { + * context.report({ + * messageId: 'e2eMessageId', + * node, + * }); + * } + * } + * } + */ + if ( + ts.isMethodDeclaration(node) && + ts.isIdentifier(node.name) && + node.name.escapedText === 'create' + ) { + return ts.factory.updateMethodDeclaration( + node, + node.decorators, + node.modifiers, + node.asteriskToken, + node.name, + node.questionToken, + node.typeParameters, + node.parameters, + node.type, + ts.factory.createBlock([ + ts.factory.createReturnStatement( + ts.factory.createObjectLiteralExpression([ + ts.factory.createMethodDeclaration( + [], + [], + undefined, + 'Program', + undefined, + [], + [ + ts.factory.createParameterDeclaration( + [], + [], + undefined, + 'node', + undefined, + undefined, + undefined + ), + ], + undefined, + ts.factory.createBlock([ + ts.factory.createExpressionStatement( + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('context'), + 'report' + ), + [], + [ + ts.factory.createObjectLiteralExpression([ + ts.factory.createPropertyAssignment( + 'messageId', + ts.factory.createStringLiteral(messageId) + ), + ts.factory.createShorthandPropertyAssignment( + 'node' + ), + ]), + ] + ) + ), + ]) + ), + ]) + ), + ]) + ); + } + + return ts.visitEachChild(node, visit, context); + } + return ts.visitNode(rootNode, visit); + }; + + const result: ts.TransformationResult = + ts.transform(newRuleSourceFile, [transformer]); + const updatedSourceFile: ts.SourceFile = result.transformed[0]; + + return printer.printFile(updatedSourceFile); +} diff --git a/packages/eslint-plugin-nx/package.json b/packages/eslint-plugin-nx/package.json index 6021ba9a8c635..b089a74dc3fbc 100644 --- a/packages/eslint-plugin-nx/package.json +++ b/packages/eslint-plugin-nx/package.json @@ -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" } } diff --git a/packages/eslint-plugin-nx/src/constants.ts b/packages/eslint-plugin-nx/src/constants.ts new file mode 100644 index 0000000000000..c1c83d2918f0f --- /dev/null +++ b/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'; diff --git a/packages/eslint-plugin-nx/src/index.ts b/packages/eslint-plugin-nx/src/index.ts index 1ded1de6fe980..7d8c77452e982 100644 --- a/packages/eslint-plugin-nx/src/index.ts +++ b/packages/eslint-plugin-nx/src/index.ts @@ -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, @@ -24,5 +27,6 @@ module.exports = { }, rules: { [enforceModuleBoundariesRuleName]: enforceModuleBoundaries, + ...workspaceRules, }, }; diff --git a/packages/eslint-plugin-nx/src/resolve-workspace-rules.ts b/packages/eslint-plugin-nx/src/resolve-workspace-rules.ts new file mode 100644 index 0000000000000..adf1a95b759c0 --- /dev/null +++ b/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>; + +/** + * 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 {}; + } +})(); diff --git a/packages/linter/generators.json b/packages/linter/generators.json new file mode 100644 index 0000000000000..6e0cd3be76fee --- /dev/null +++ b/packages/linter/generators.json @@ -0,0 +1,30 @@ +{ + "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" + } + }, + "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" + } + } +} diff --git a/packages/linter/package.json b/packages/linter/package.json index 49ef460448766..2d40650303365 100644 --- a/packages/linter/package.json +++ b/packages/linter/package.json @@ -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", diff --git a/packages/linter/src/generators/workspace-rule/__snapshots__/workspace-rule.spec.ts.snap b/packages/linter/src/generators/workspace-rule/__snapshots__/workspace-rule.spec.ts.snap new file mode 100644 index 0000000000000..ec63f92164421 --- /dev/null +++ b/packages/linter/src/generators/workspace-rule/__snapshots__/workspace-rule.spec.ts.snap @@ -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 +} +}; +" +`; diff --git a/packages/linter/src/generators/workspace-rule/files/__name__.spec.ts__tmpl__ b/packages/linter/src/generators/workspace-rule/files/__name__.spec.ts__tmpl__ new file mode 100644 index 0000000000000..61219da65f337 --- /dev/null +++ b/packages/linter/src/generators/workspace-rule/files/__name__.spec.ts__tmpl__ @@ -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: [], +}); diff --git a/packages/linter/src/generators/workspace-rule/files/__name__.ts__tmpl__ b/packages/linter/src/generators/workspace-rule/files/__name__.ts__tmpl__ new file mode 100644 index 0000000000000..52032ca54706d --- /dev/null +++ b/packages/linter/src/generators/workspace-rule/files/__name__.ts__tmpl__ @@ -0,0 +1,38 @@ +/** + * 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/<%= name %>" +export const RULE_NAME = '<%= name %>'; + +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 {}; + }, +}); diff --git a/packages/linter/src/generators/workspace-rule/schema.json b/packages/linter/src/generators/workspace-rule/schema.json new file mode 100644 index 0000000000000..c5716a848b968 --- /dev/null +++ b/packages/linter/src/generators/workspace-rule/schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "NxWorkspaceRule", + "cli": "nx", + "title": "Create a new Workspace Lint Rule", + "description": "Create a new Workspace Lint Rule", + "type": "object", + "examples": [ + { + "command": "g @nrwl/linter:workspace-rule my-custom-rule", + "description": "Create a new workspace lint rule called my-custom-rule" + }, + { + "command": "g @nrwl/linter:workspace-rule --name=my-custom-rule --directory=a/b/c", + "description": "Create a new workspace lint rule located at tools/eslint-rules/a/b/c/my-custom-rule.ts" + } + ], + "properties": { + "name": { + "type": "string", + "description": "The name of the new rule", + "$default": { + "$source": "argv", + "index": 0 + } + }, + "directory": { + "type": "string", + "description": "Create the rule under this directory within tools/eslint-rules/ (can be nested).", + "alias": "dir", + "default": "rules" + } + }, + "required": ["name", "directory"] +} diff --git a/packages/linter/src/generators/workspace-rule/workspace-rule.spec.ts b/packages/linter/src/generators/workspace-rule/workspace-rule.spec.ts new file mode 100644 index 0000000000000..f50e9ced376f4 --- /dev/null +++ b/packages/linter/src/generators/workspace-rule/workspace-rule.spec.ts @@ -0,0 +1,185 @@ +import { Tree } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { lintWorkspaceRuleGenerator } from './workspace-rule'; + +describe('@nrwl/linter:workspace-rule', () => { + let tree: Tree; + + beforeEach(async () => { + tree = createTreeWithEmptyWorkspace(); + }); + + it('should generate the required files', async () => { + await lintWorkspaceRuleGenerator(tree, { + name: 'my-rule', + directory: 'rules', + }); + + expect( + tree.read('tools/eslint-rules/rules/my-rule.ts', 'utf-8') + ).toMatchSnapshot(); + + expect( + tree.read('tools/eslint-rules/rules/my-rule.spec.ts', 'utf-8') + ).toMatchSnapshot(); + }); + + it('should update the plugin index.ts with the new rule', async () => { + await lintWorkspaceRuleGenerator(tree, { + name: 'my-rule', + directory: 'rules', + }); + + // NOTE: formatFiles() will have been run so the real formatting will look better than this snapshot + expect(tree.read('tools/eslint-rules/index.ts', 'utf-8')).toMatchSnapshot(); + }); + + it('should update the plugin index.ts with the new rule correctly, regardless of whether the existing rules config has a trailing comma', async () => { + // ------------------------------------------- NO EXISTING RULE + + tree.write( + 'tools/eslint-rules/index.ts', + ` + module.exports = { + rules: {} + }; + ` + ); + + await lintWorkspaceRuleGenerator(tree, { + name: 'my-rule', + directory: 'rules', + }); + + // NOTE: formatFiles() will have been run so the real formatting will look better than this snapshot + expect(tree.read('tools/eslint-rules/index.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "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 + } + }; + " + `); + + // ------------------------------------------- EXISTING RULE, NO TRAILING COMMA + + tree.write( + 'tools/eslint-rules/index.ts', + ` + module.exports = { + rules: { + 'existing-rule-no-comma': 'error' + } + }; + ` + ); + + await lintWorkspaceRuleGenerator(tree, { + name: 'my-rule', + directory: 'rules', + }); + + // NOTE: formatFiles() will have been run so the real formatting will look better than this snapshot + expect(tree.read('tools/eslint-rules/index.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "import { RULE_NAME as myRuleName, rule as myRule } from './rules/my-rule'; + + module.exports = { + rules: { + 'existing-rule-no-comma': 'error' + ,[myRuleName]: myRule + } + }; + " + `); + + // ------------------------------------------- EXISTING RULE, WITH TRAILING COMMA + + tree.write( + 'tools/eslint-rules/index.ts', + ` + module.exports = { + rules: { + 'existing-rule-with-comma': 'error', + } + }; + ` + ); + + await lintWorkspaceRuleGenerator(tree, { + name: 'my-rule', + directory: 'rules', + }); + + // NOTE: formatFiles() will have been run so the real formatting will look better than this snapshot + expect(tree.read('tools/eslint-rules/index.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "import { RULE_NAME as myRuleName, rule as myRule } from './rules/my-rule'; + + module.exports = { + rules: { + 'existing-rule-with-comma': 'error', + [myRuleName]: myRule + } + }; + " + `); + }); + + describe('--dir', () => { + it('should support creating the rule in a nested directory', async () => { + await lintWorkspaceRuleGenerator(tree, { + name: 'another-rule', + directory: 'some-dir', + }); + + expect( + tree.read('tools/eslint-rules/some-dir/another-rule.ts', 'utf-8') + ).toMatchSnapshot(); + + expect( + tree.read('tools/eslint-rules/some-dir/another-rule.spec.ts', 'utf-8') + ).toMatchSnapshot(); + }); + + it('should support creating the rule in a nested directory with multiple levels of nesting', async () => { + await lintWorkspaceRuleGenerator(tree, { + name: 'one-more-rule', + directory: 'a/b/c', + }); + + expect( + tree.read('tools/eslint-rules/a/b/c/one-more-rule.ts', 'utf-8') + ).toMatchSnapshot(); + + expect( + tree.read('tools/eslint-rules/a/b/c/one-more-rule.spec.ts', 'utf-8') + ).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/linter/src/generators/workspace-rule/workspace-rule.ts b/packages/linter/src/generators/workspace-rule/workspace-rule.ts new file mode 100644 index 0000000000000..fe7ede7de94e6 --- /dev/null +++ b/packages/linter/src/generators/workspace-rule/workspace-rule.ts @@ -0,0 +1,120 @@ +import { + applyChangesToString, + ChangeType, + convertNxGenerator, + formatFiles, + generateFiles, + joinPathFragments, + logger, + Tree, +} from '@nrwl/devkit'; +import { camelize } from '@nrwl/workspace/src/utils/strings'; +import { join } from 'path'; +import * as ts from 'typescript'; +import { workspaceLintPluginDir } from '../../utils/workspace-lint-rules'; +import { lintWorkspaceRulesProjectGenerator } from '../workspace-rules-project/workspace-rules-project'; + +export interface LintWorkspaceRuleGeneratorOptions { + name: string; + directory: string; +} + +export async function lintWorkspaceRuleGenerator( + tree: Tree, + options: LintWorkspaceRuleGeneratorOptions +) { + // Ensure that the workspace rules project has been created + const projectGeneratorCallback = await lintWorkspaceRulesProjectGenerator( + tree + ); + + const ruleDir = joinPathFragments( + workspaceLintPluginDir, + options.directory ?? '' + ); + + // Generate the required files for the new rule + generateFiles(tree, join(__dirname, 'files'), ruleDir, { + tmpl: '', + name: options.name, + }); + + const nameCamelCase = camelize(options.name); + + /** + * Import the new rule into the workspace plugin index.ts and + * register it ready for use in .eslintrc.json configs. + */ + const pluginIndexPath = joinPathFragments(workspaceLintPluginDir, 'index.ts'); + const existingPluginIndexContents = tree.read(pluginIndexPath, 'utf-8'); + const pluginIndexSourceFile = ts.createSourceFile( + pluginIndexPath, + existingPluginIndexContents, + ts.ScriptTarget.Latest, + true + ); + + function findRulesObject(node: ts.Node): ts.ObjectLiteralExpression { + if ( + ts.isPropertyAssignment(node) && + ts.isIdentifier(node.name) && + node.name.text === 'rules' && + ts.isObjectLiteralExpression(node.initializer) + ) { + return node.initializer; + } + + return node.forEachChild(findRulesObject); + } + + const rulesObject = pluginIndexSourceFile.forEachChild((node) => + findRulesObject(node) + ); + if (rulesObject) { + const ruleNameSymbol = `${nameCamelCase}Name`; + const ruleConfigSymbol = nameCamelCase; + + /** + * If the rules object already has entries, we need to make sure our insertion + * takes commas into account. + */ + let leadingComma = ''; + if (rulesObject.properties.length > 0) { + if (!rulesObject.properties.hasTrailingComma) { + leadingComma = ','; + } + } + + const newContents = applyChangesToString(existingPluginIndexContents, [ + { + type: ChangeType.Insert, + index: 0, + text: `import { RULE_NAME as ${ruleNameSymbol}, rule as ${ruleConfigSymbol} } from './${ + options.directory ? `${options.directory}/` : '' + }${options.name}';\n`, + }, + { + type: ChangeType.Insert, + index: rulesObject.getEnd() - 1, + text: `${leadingComma}[${ruleNameSymbol}]: ${ruleConfigSymbol}\n`, + }, + ]); + + tree.write(pluginIndexPath, newContents); + } + + await formatFiles(tree); + + logger.info(`NX Reminder: Once you have finished writing your rule logic, you need to actually enable the rule within an appropriate .eslintrc.json in your workspace, for example: + + "rules": { + "@nrwl/nx/workspace/${options.name}": "error" + } +`); + + return projectGeneratorCallback; +} + +export const lintWorkspaceRuleSchematic = convertNxGenerator( + lintWorkspaceRuleGenerator +); diff --git a/packages/linter/src/generators/workspace-rules-project/__snapshots__/workspace-rules-project.spec.ts.snap b/packages/linter/src/generators/workspace-rules-project/__snapshots__/workspace-rules-project.spec.ts.snap new file mode 100644 index 0000000000000..936fcd23d253a --- /dev/null +++ b/packages/linter/src/generators/workspace-rules-project/__snapshots__/workspace-rules-project.spec.ts.snap @@ -0,0 +1,99 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`@nrwl/linter:workspace-rules-project 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 './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: {} +}; +" +`; + +exports[`@nrwl/linter:workspace-rules-project should generate the required files 2`] = ` +"{ + \\"extends\\": \\"../../tsconfig.base.json\\", + \\"compilerOptions\\": { + \\"module\\": \\"commonjs\\" + }, + \\"files\\": [], + \\"include\\": [], + \\"references\\": [ + { + \\"path\\": \\"./tsconfig.lint.json\\" + }, + { + \\"path\\": \\"./tsconfig.spec.json\\" + } + ] +} +" +`; + +exports[`@nrwl/linter:workspace-rules-project should generate the required files 3`] = ` +"{ + \\"extends\\": \\"./tsconfig.json\\", + \\"compilerOptions\\": { + \\"outDir\\": \\"../../dist/out-tsc\\", + \\"types\\": [\\"node\\"] + }, + \\"exclude\\": [\\"**/*.spec.ts\\"], + \\"include\\": [\\"**/*.ts\\"] +} +" +`; + +exports[`@nrwl/linter:workspace-rules-project should generate the required files 4`] = ` +"{ + \\"extends\\": \\"./tsconfig.json\\", + \\"compilerOptions\\": { + \\"outDir\\": \\"../../dist/out-tsc\\", + \\"module\\": \\"commonjs\\", + \\"types\\": [\\"jest\\", \\"node\\"] + }, + \\"include\\": [ + \\"**/*.spec.ts\\", + \\"**/*.d.ts\\" + ] +} +" +`; + +exports[`@nrwl/linter:workspace-rules-project should generate the required files 5`] = ` +"module.exports = { + displayName: 'eslint-rules', + preset: '../../jest.preset.js', + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + } + }, + transform: { + '^.+\\\\\\\\.[tj]s$': 'ts-jest' + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../coverage/tools/eslint-rules' +}; +" +`; diff --git a/packages/linter/src/generators/workspace-rules-project/files/index.ts__tmpl__ b/packages/linter/src/generators/workspace-rules-project/files/index.ts__tmpl__ new file mode 100644 index 0000000000000..79c806b39d943 --- /dev/null +++ b/packages/linter/src/generators/workspace-rules-project/files/index.ts__tmpl__ @@ -0,0 +1,27 @@ +/** + * 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: {} +}; diff --git a/packages/linter/src/generators/workspace-rules-project/files/tsconfig.json__tmpl__ b/packages/linter/src/generators/workspace-rules-project/files/tsconfig.json__tmpl__ new file mode 100644 index 0000000000000..10bb5c8c3d42d --- /dev/null +++ b/packages/linter/src/generators/workspace-rules-project/files/tsconfig.json__tmpl__ @@ -0,0 +1,13 @@ +{ + "extends": "<%= offsetFromRoot %>tsconfig.base.json", + "compilerOptions": { + "module": "commonjs" + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lint.json" + } + ] +} diff --git a/packages/linter/src/generators/workspace-rules-project/files/tsconfig.lint.json__tmpl__ b/packages/linter/src/generators/workspace-rules-project/files/tsconfig.lint.json__tmpl__ new file mode 100644 index 0000000000000..ee0f68db8873b --- /dev/null +++ b/packages/linter/src/generators/workspace-rules-project/files/tsconfig.lint.json__tmpl__ @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "<%= offsetFromRoot %>dist/out-tsc", + "types": ["node"] + }, + "exclude": ["**/*.spec.ts"], + "include": ["**/*.ts"] +} diff --git a/packages/linter/src/generators/workspace-rules-project/schema.json b/packages/linter/src/generators/workspace-rules-project/schema.json new file mode 100644 index 0000000000000..2757ad3950951 --- /dev/null +++ b/packages/linter/src/generators/workspace-rules-project/schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "NxWorkspaceRulesProject", + "cli": "nx", + "title": "Create the Workspace Lint Rules Project", + "description": "Create the Workspace Lint Rules Project", + "type": "object", + "examples": [ + { + "command": "g @nrwl/linter:workspace-rules-project", + "description": "Create the Workspace Lint Rules Project" + } + ], + "properties": {}, + "required": [] +} diff --git a/packages/linter/src/generators/workspace-rules-project/workspace-rules-project.spec.ts b/packages/linter/src/generators/workspace-rules-project/workspace-rules-project.spec.ts new file mode 100644 index 0000000000000..026d6dc757d03 --- /dev/null +++ b/packages/linter/src/generators/workspace-rules-project/workspace-rules-project.spec.ts @@ -0,0 +1,105 @@ +import { + addProjectConfiguration, + readJson, + readProjectConfiguration, + Tree, +} from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { + lintWorkspaceRulesProjectGenerator, + WORKSPACE_RULES_PROJECT_NAME, +} from './workspace-rules-project'; + +describe('@nrwl/linter:workspace-rules-project', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + }); + + it('should update implicitDependencies in nx.json', async () => { + expect( + readJson(tree, 'nx.json').implicitDependencies + ).toMatchInlineSnapshot(`undefined`); + + await lintWorkspaceRulesProjectGenerator(tree); + + expect(readJson(tree, 'nx.json').implicitDependencies) + .toMatchInlineSnapshot(` + Object { + "tools/eslint-rules/**/*": "*", + } + `); + }); + + it('should generate the required files', async () => { + await lintWorkspaceRulesProjectGenerator(tree); + + expect(tree.read('tools/eslint-rules/index.ts', 'utf-8')).toMatchSnapshot(); + expect( + tree.read('tools/eslint-rules/tsconfig.json', 'utf-8') + ).toMatchSnapshot(); + expect( + tree.read('tools/eslint-rules/tsconfig.lint.json', 'utf-8') + ).toMatchSnapshot(); + expect( + tree.read('tools/eslint-rules/tsconfig.spec.json', 'utf-8') + ).toMatchSnapshot(); + expect( + tree.read('tools/eslint-rules/jest.config.js', 'utf-8') + ).toMatchSnapshot(); + }); + + it('should create a project with a test target', async () => { + await lintWorkspaceRulesProjectGenerator(tree); + + expect(readProjectConfiguration(tree, WORKSPACE_RULES_PROJECT_NAME)) + .toMatchInlineSnapshot(` + Object { + "root": "tools/eslint-rules", + "sourceRoot": "tools/eslint-rules", + "targets": Object { + "test": Object { + "executor": "@nrwl/jest:jest", + "options": Object { + "jestConfig": "tools/eslint-rules/jest.config.js", + "passWithNoTests": true, + }, + "outputs": Array [ + "coverage/tools/eslint-rules", + ], + }, + }, + } + `); + }); + + it('should not update the required files if the project already exists', async () => { + addProjectConfiguration(tree, WORKSPACE_RULES_PROJECT_NAME, { + root: '', + }); + + 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); + tree.write('tools/eslint-rules/tsconfig.lint.json', customTsconfigContents); + tree.write('tools/eslint-rules/tsconfig.spec.json', customTsconfigContents); + + await lintWorkspaceRulesProjectGenerator(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 + ); + expect(tree.read('tools/eslint-rules/tsconfig.lint.json', 'utf-8')).toEqual( + customTsconfigContents + ); + expect(tree.read('tools/eslint-rules/tsconfig.spec.json', 'utf-8')).toEqual( + customTsconfigContents + ); + }); +}); diff --git a/packages/linter/src/generators/workspace-rules-project/workspace-rules-project.ts b/packages/linter/src/generators/workspace-rules-project/workspace-rules-project.ts new file mode 100644 index 0000000000000..153e789b5ccd9 --- /dev/null +++ b/packages/linter/src/generators/workspace-rules-project/workspace-rules-project.ts @@ -0,0 +1,64 @@ +import { + addProjectConfiguration, + convertNxGenerator, + generateFiles, + offsetFromRoot, + readProjectConfiguration, + readWorkspaceConfiguration, + Tree, + updateWorkspaceConfiguration, +} from '@nrwl/devkit'; +import { jestProjectGenerator } from '@nrwl/jest'; +import { join } from 'path'; +import { workspaceLintPluginDir } from '../../utils/workspace-lint-rules'; + +export const WORKSPACE_RULES_PROJECT_NAME = 'eslint-rules'; + +const WORKSPACE_PLUGIN_DIR = 'tools/eslint-rules'; + +export async function lintWorkspaceRulesProjectGenerator(tree: Tree) { + // Noop if the workspace rules project already exists + try { + readProjectConfiguration(tree, WORKSPACE_RULES_PROJECT_NAME); + return; + } catch {} + + // Create the project, the test target is added below by the jest generator + addProjectConfiguration(tree, WORKSPACE_RULES_PROJECT_NAME, { + root: WORKSPACE_PLUGIN_DIR, + sourceRoot: WORKSPACE_PLUGIN_DIR, + targets: {}, + }); + + // Generate the required files + generateFiles(tree, join(__dirname, 'files'), workspaceLintPluginDir, { + tmpl: '', + offsetFromRoot: offsetFromRoot(WORKSPACE_PLUGIN_DIR), + }); + + /** + * Ensure that when workspace rules are updated they cause all projects to be affected for now. + * TODO: Explore writing a ProjectGraph plugin to make this more surgical. + */ + const workspaceConfig = readWorkspaceConfiguration(tree); + updateWorkspaceConfiguration(tree, { + ...workspaceConfig, + implicitDependencies: { + ...workspaceConfig.implicitDependencies, + [`${WORKSPACE_PLUGIN_DIR}/**/*`]: '*', + }, + }); + + // Add jest to the project and return installation task + return await jestProjectGenerator(tree, { + project: WORKSPACE_RULES_PROJECT_NAME, + supportTsx: false, + skipSerializers: true, + setupFile: 'none', + babelJest: false, + }); +} + +export const lintWorkspaceRulesProjectSchematic = convertNxGenerator( + lintWorkspaceRulesProjectGenerator +); diff --git a/packages/linter/src/utils/workspace-lint-rules.ts b/packages/linter/src/utils/workspace-lint-rules.ts new file mode 100644 index 0000000000000..4d8dab15c776c --- /dev/null +++ b/packages/linter/src/utils/workspace-lint-rules.ts @@ -0,0 +1,3 @@ +import { joinPathFragments } from '@nrwl/devkit'; + +export const workspaceLintPluginDir = joinPathFragments('tools/eslint-rules');