From 2306941eda7e63632ba5b5e00eec5613567bc203 Mon Sep 17 00:00:00 2001 From: sharvandeep <2400032427@kluniversity.in> Date: Thu, 30 Oct 2025 21:27:43 +0530 Subject: [PATCH] feat(eslint): add no-jsx-text-literals rule + tests (fixes #6472) Signed-off-by: sharvandeep <2400032427@kluniversity.in> --- .../eslint-plugin-docusaurus/lib/index.js | 5 + .../lib/rules/no-jsx-text-literals.js | 93 +++++++++++++++++++ .../eslint-plugin-docusaurus/package.json | 13 +++ .../tests/no-jsx-text-literals.test.js | 31 +++++++ 4 files changed, 142 insertions(+) create mode 100644 packages/eslint-plugin-docusaurus/lib/index.js create mode 100644 packages/eslint-plugin-docusaurus/lib/rules/no-jsx-text-literals.js create mode 100644 packages/eslint-plugin-docusaurus/package.json create mode 100644 packages/eslint-plugin-docusaurus/tests/no-jsx-text-literals.test.js diff --git a/packages/eslint-plugin-docusaurus/lib/index.js b/packages/eslint-plugin-docusaurus/lib/index.js new file mode 100644 index 0000000000..e55f36f9a1 --- /dev/null +++ b/packages/eslint-plugin-docusaurus/lib/index.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + 'no-jsx-text-literals': require('./rules/no-jsx-text-literals'), + }, +}; diff --git a/packages/eslint-plugin-docusaurus/lib/rules/no-jsx-text-literals.js b/packages/eslint-plugin-docusaurus/lib/rules/no-jsx-text-literals.js new file mode 100644 index 0000000000..12da758868 --- /dev/null +++ b/packages/eslint-plugin-docusaurus/lib/rules/no-jsx-text-literals.js @@ -0,0 +1,93 @@ +/** + * @fileoverview Disallow plain text literals in JSX to encourage use of i18n/Translate + */ + +'use strict'; + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'disallow literal text in JSX (encourage or i18n usage)', + category: 'Best Practices', + recommended: false, + }, + schema: [], // no options + messages: { + noJsxText: 'Avoid using raw text in JSX — wrap translatable text with or use i18n APIs.', + }, + }, + + create(context) { + // helper: find nearest ancestor JSX element name (if any) + function findAncestorElementName(node) { + let p = node.parent; + while (p) { + if (p.type === 'JSXElement' && p.openingElement && p.openingElement.name) { + const nameNode = p.openingElement.name; + // handle simple identifier names only (e.g., Translate, div) + if (nameNode.type === 'JSXIdentifier' && nameNode.name) { + return nameNode.name; + } + // handle MemberExpressions like Some.Namespace.Component (rare in JSX) + if (nameNode.type === 'JSXMemberExpression') { + let parts = []; + let curr = nameNode; + while (curr) { + if (curr.property && curr.property.name) parts.unshift(curr.property.name); + if (curr.object && curr.object.name) { parts.unshift(curr.object.name); break; } + curr = curr.object; + } + return parts.join('.'); + } + } + p = p.parent; + } + return null; + } + + const ignoreTags = new Set(['code', 'pre', 'script', 'style', 'Translate']); + + return { + JSXText(node) { + if (!node || !node.value) { + return; + } + const text = node.value.trim(); + if (!text) { + return; + } + + // If inside an ignored tag (including Translate), skip + const ancestorName = findAncestorElementName(node); + if (ancestorName && ignoreTags.has(ancestorName)) { + return; + } + + context.report({ + node, + messageId: 'noJsxText', + }); + }, + + Literal(node) { + // Flag string literals used as children: e.g. { 'Hello' } + if (typeof node.value === 'string' && node.parent && node.parent.type === 'JSXExpressionContainer') { + const text = node.value.trim(); + if (!text) return; + + // If inside ignored tag (including Translate), skip + const ancestorName = findAncestorElementName(node); + if (ancestorName && ignoreTags.has(ancestorName)) { + return; + } + + context.report({ + node, + messageId: 'noJsxText', + }); + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin-docusaurus/package.json b/packages/eslint-plugin-docusaurus/package.json new file mode 100644 index 0000000000..18423f6c8b --- /dev/null +++ b/packages/eslint-plugin-docusaurus/package.json @@ -0,0 +1,13 @@ +{ + "name": "eslint-plugin-docusaurus-local", + "version": "0.0.0", + "private": true, + "main": "lib/index.js", + "scripts": { + "test": "mocha tests/*.test.js --timeout 5000" + }, + "devDependencies": { + "eslint": "^8.57.1", + "mocha": "^10.8.2" + } +} diff --git a/packages/eslint-plugin-docusaurus/tests/no-jsx-text-literals.test.js b/packages/eslint-plugin-docusaurus/tests/no-jsx-text-literals.test.js new file mode 100644 index 0000000000..23f7a58281 --- /dev/null +++ b/packages/eslint-plugin-docusaurus/tests/no-jsx-text-literals.test.js @@ -0,0 +1,31 @@ +'use strict'; + +const RuleTester = require('eslint').RuleTester; +const rule = require('../lib/rules/no-jsx-text-literals'); + +const ruleTester = new RuleTester({ + parserOptions: { ecmaVersion: 2020, sourceType: 'module', ecmaFeatures: { jsx: true } }, +}); + +ruleTester.run('no-jsx-text-literals', rule, { + valid: [ + // using Translate component + { code: "const a = Hello;" }, + // whitespace-only + { code: "const a =
;" }, + // code/pre tags + { code: "const a =
some code
;" }, + // dynamic expression + { code: "const a =
{message}
;" }, + ], + invalid: [ + { + code: "const a =
Hello world
;", + errors: [{ messageId: 'noJsxText' }], + }, + { + code: "const a =

{'Plain text'}

;", + errors: [{ messageId: 'noJsxText' }], + }, + ], +});