-
diff --git a/tools/eslint-rules/index.ts b/tools/eslint-rules/index.ts
index 54ac86050bc7..3367634ce323 100644
--- a/tools/eslint-rules/index.ts
+++ b/tools/eslint-rules/index.ts
@@ -22,6 +22,10 @@ import {
rule as noHardcodedColors,
RULE_NAME as noHardcodedColorsName,
} from './rules/no-hardcoded-colors';
+import {
+ rule as noNavigatePreferLink,
+ RULE_NAME as noNavigatePreferLinkName,
+} from './rules/no-navigate-prefer-link';
import {
rule as noStateUseref,
RULE_NAME as noStateUserefName,
@@ -83,5 +87,6 @@ module.exports = {
[maxConstsPerFileName]: maxConstsPerFile,
[useRecoilCallbackHasDependencyArrayName]:
useRecoilCallbackHasDependencyArray,
+ [noNavigatePreferLinkName]: noNavigatePreferLink,
},
};
diff --git a/tools/eslint-rules/rules/no-navigate-prefer-link.spec.ts b/tools/eslint-rules/rules/no-navigate-prefer-link.spec.ts
new file mode 100644
index 000000000000..e8daa76b21ea
--- /dev/null
+++ b/tools/eslint-rules/rules/no-navigate-prefer-link.spec.ts
@@ -0,0 +1,59 @@
+import { TSESLint } from '@typescript-eslint/utils';
+
+import { rule, RULE_NAME } from './no-navigate-prefer-link';
+
+const ruleTester = new TSESLint.RuleTester({
+ parser: require.resolve('@typescript-eslint/parser'),
+});
+
+ruleTester.run(RULE_NAME, rule, {
+ valid: [
+ {
+ code: 'if(someVar) { navigate("/"); }',
+ },
+ {
+ code: 'Click me',
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ },
+ {
+ code: '{ navigate("/"); doSomething(); }} />',
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ },
+ ],
+ invalid: [
+ {
+ code: ' navigate("/")} />',
+ errors: [
+ {
+ messageId: 'preferLink',
+ },
+ ],
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ },
+ {
+ code: ' { navigate("/");} } />',
+ errors: [
+ {
+ messageId: 'preferLink',
+ },
+ ],
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ },
+ ],
+});
diff --git a/tools/eslint-rules/rules/no-navigate-prefer-link.ts b/tools/eslint-rules/rules/no-navigate-prefer-link.ts
new file mode 100644
index 000000000000..80eaf36d0506
--- /dev/null
+++ b/tools/eslint-rules/rules/no-navigate-prefer-link.ts
@@ -0,0 +1,101 @@
+import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';
+
+// NOTE: The rule will be available in ESLint configs as "@nx/workspace-no-navigate-prefer-link"
+export const RULE_NAME = 'no-navigate-prefer-link';
+
+export const rule = ESLintUtils.RuleCreator(() => __filename)({
+ name: RULE_NAME,
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description:
+ 'Discourage usage of navigate() where a simple component would suffice.',
+ },
+ messages: {
+ preferLink: 'Use instead of navigate() for pure navigation.',
+ },
+ schema: [],
+ },
+ defaultOptions: [],
+ create: (context) => {
+ const functionMap: Record = {};
+
+ const checkFunctionBodyHasSingleNavigateCall = (
+ func: TSESTree.ArrowFunctionExpression,
+ ) => {
+ // Check for simple arrow function with single navigate call
+ if (
+ func.body.type === 'CallExpression' &&
+ func.body.callee.type === 'Identifier' &&
+ func.body.callee.name === 'navigate'
+ ) {
+ return true;
+ }
+
+ // Check for block arrow function with single navigate call
+ if (
+ func.body.type === 'BlockStatement' &&
+ func.body.body.length === 1 &&
+ func.body.body[0].type === 'ExpressionStatement' &&
+ func.body.body[0].expression.type === 'CallExpression' &&
+ func.body.body[0].expression.callee.type === 'Identifier' &&
+ func.body.body[0].expression.callee.name === 'navigate'
+ ) {
+ return true;
+ }
+
+ return false;
+ };
+
+ return {
+ VariableDeclarator: (node) => {
+ // Check for function declaration on onClick
+ if (
+ node.init &&
+ node.init.type === 'ArrowFunctionExpression' &&
+ node.id.type === 'Identifier'
+ ) {
+ const func = node.init;
+ functionMap[node.id.name] = func;
+
+ if (checkFunctionBodyHasSingleNavigateCall(func)) {
+ context.report({
+ node: func,
+ messageId: 'preferLink',
+ });
+ }
+ }
+ },
+ JSXAttribute: (node) => {
+ // Check for navigate call directly on onClick
+ if (
+ node.name.name === 'onClick' &&
+ node.value.type === 'JSXExpressionContainer'
+ ) {
+ const expression = node.value.expression;
+
+ if (
+ expression.type === 'ArrowFunctionExpression' &&
+ checkFunctionBodyHasSingleNavigateCall(expression)
+ ) {
+ context.report({
+ node: expression,
+ messageId: 'preferLink',
+ });
+ } else if (
+ expression.type === 'Identifier' &&
+ functionMap[expression.name]
+ ) {
+ const func = functionMap[expression.name];
+ if (checkFunctionBodyHasSingleNavigateCall(func)) {
+ context.report({
+ node: expression,
+ messageId: 'preferLink',
+ });
+ }
+ }
+ }
+ },
+ };
+ },
+});