UNPKG

@generaltranslation/gt-next-lint

Version:

ESLint plugin for General Translation Next.js integration

209 lines 9.49 kB
"use strict"; /** * ESLint rule: no-dynamic-jsx * * Detects unwrapped dynamic content in GT-Next translation components. * Equivalent to the SWC plugin functionality but with proper ESLint error reporting. * * This rule checks for JSX expressions ({dynamic content}) inside <T> components * that are not wrapped in variable components (<Var>, <DateTime>, <Num>, <Currency>). */ Object.defineProperty(exports, "__esModule", { value: true }); exports.noDynamicJsx = void 0; const GT_MODULES = ['gt-next', 'gt-next/client', 'gt-next/server']; const TRANSLATION_COMPONENTS = ['T']; const VARIABLE_COMPONENTS = ['Var', 'DateTime', 'Num', 'Currency']; function isGTModule(source) { return GT_MODULES.includes(source); } function isTranslationComponent(name) { return TRANSLATION_COMPONENTS.includes(name); } function isVariableComponent(name) { return VARIABLE_COMPONENTS.includes(name); } exports.noDynamicJsx = { meta: { type: 'problem', docs: { description: 'Detect unwrapped dynamic content in GT-Next translation components', category: 'Best Practices', recommended: true, url: 'https://github.com/generaltranslation/gt/tree/main/packages/next-lint#no-dynamic-jsx', }, fixable: undefined, schema: [], messages: { dynamicJsx: 'Dynamic content in <T> component should be wrapped in a variable component (<Var>, <DateTime>, <Num>, or <Currency>)', }, }, create(context) { const state = { translationStack: 0, variableStack: 0, imports: { translationComponents: new Set(), variableComponents: new Set(), namespaceImports: new Set(), assignedTranslationComponents: new Set(), assignedVariableComponents: new Set(), }, }; function getElementName(node) { const name = node?.openingElement?.name; if (!name) return null; if (name.type === 'JSXIdentifier') { return name.name; } if (name.type === 'JSXMemberExpression') { const obj = name.object; const prop = name.property; if (obj?.type === 'JSXIdentifier' && prop?.type === 'JSXIdentifier') { return `${obj.name}.${prop.name}`; } } return null; } function isNamespaceTranslationComponent(elementName) { const parts = elementName.split('.'); if (parts.length === 2) { const [namespace, component] = parts; return (state.imports.namespaceImports.has(namespace) && isTranslationComponent(component)); } return false; } function isNamespaceVariableComponent(elementName) { const parts = elementName.split('.'); if (parts.length === 2) { const [namespace, component] = parts; return (state.imports.namespaceImports.has(namespace) && isVariableComponent(component)); } return false; } function isInsideJSXAttribute(node) { // Walk up the AST to check if this expression is inside a JSX attribute let currentNode = node.parent; while (currentNode) { if (currentNode.type === 'JSXAttribute') { return true; } // If we reach a JSX element, we're in element content, not attributes if (currentNode.type === 'JSXElement') { return false; } currentNode = currentNode.parent; } return false; } // Check if the node is a string literal or template literal function isStringLiteral(expression) { // For JSXExpressionContainer, check the expression inside return ((expression.type === 'Literal' && typeof expression.value === 'string') || (expression.type === 'TemplateLiteral' && expression.expressions.length === 0) // No interpolation ); } // Check if the node is a comment function isComment(expression) { return expression.type === 'Block' || expression.type === 'Line'; } // Check if empty function isEmpty(expression) { return expression.type === 'JSXEmptyExpression'; } return { // Track imports from GT-Next modules ImportDeclaration(node) { if (node.source?.type === 'Literal' && typeof node.source.value === 'string') { const source = node.source.value; if (isGTModule(source)) { for (const specifier of node.specifiers || []) { if (specifier.type === 'ImportSpecifier') { const importedName = specifier.imported?.name || ''; const localName = specifier.local?.name || ''; if (isTranslationComponent(importedName)) { state.imports.translationComponents.add(localName); } else if (isVariableComponent(importedName)) { state.imports.variableComponents.add(localName); } } else if (specifier.type === 'ImportNamespaceSpecifier') { const localName = specifier.local?.name || ''; state.imports.namespaceImports.add(localName); } } } } }, // Track variable assignments from GT components VariableDeclarator(node) { if (node.id?.type === 'Identifier' && node.init?.type === 'Identifier') { const varName = node.id.name; const assignedFrom = node.init.name; if (state.imports.translationComponents.has(assignedFrom)) { state.imports.assignedTranslationComponents.add(varName); } else if (state.imports.variableComponents.has(assignedFrom)) { state.imports.assignedVariableComponents.add(varName); } } }, // Detect unwrapped dynamic content JSXExpressionContainer(node) { // Skip expressions inside JSX attributes (e.g., <Image width={16} />) if (isInsideJSXAttribute(node)) { return; } // Skip expressions with just a string literal (e.g., {'Hello'}) // Skip expressions with just a comment // Skip expressions with just an empty statement if ((node.expression && isStringLiteral(node.expression)) || isComment(node.expression) || isEmpty(node.expression)) { return; } // Check if this expression is inside a translation component but not inside a variable component let inTranslationComponent = false; let inVariableComponent = false; // Walk up the AST to find parent JSX elements let currentNode = node.parent; while (currentNode) { if (currentNode.type === 'JSXElement') { const elementName = getElementName(currentNode); if (elementName) { // Check if this is a variable component if (state.imports.variableComponents.has(elementName) || state.imports.assignedVariableComponents.has(elementName) || isNamespaceVariableComponent(elementName)) { inVariableComponent = true; break; // If we find a variable component, we don't need to check further } // Check if this is a translation component else if (state.imports.translationComponents.has(elementName) || state.imports.assignedTranslationComponents.has(elementName) || isNamespaceTranslationComponent(elementName)) { inTranslationComponent = true; } } } currentNode = currentNode.parent; } // Report if we're inside a translation component but not inside a variable component if (inTranslationComponent && !inVariableComponent) { context.report({ node, messageId: 'dynamicJsx', }); } }, }; }, }; //# sourceMappingURL=no-dynamic-jsx.js.map