diff --git a/index.js b/index.js index 96b62a6..35239f4 100644 --- a/index.js +++ b/index.js @@ -11,33 +11,37 @@ function getJSXElementName(jsx) { } } -function handleJSX(context, name, jsx) { +function handleJSX(context, name, exported, jsx) { if ( !providerRegex.test(getJSXElementName(jsx)) && !jsx.openingElement.attributes.find( (a) => a.name?.name === 'data-component', ) ) { - context.report({ - node: jsx, - message: `${name} is missing the data-component attribute for the top-level element.`, - fix(fixer) { - return fixer.insertTextAfterRange( - jsx.openingElement.typeParameters - ? jsx.openingElement.typeParameters.range - : jsx.openingElement.name.range, - ` data-component="${name}"`, - ); + context.possibleReports.push({ + name, + exported, + report: { + node: jsx.openingElement, + message: `${name} is missing the data-component attribute for the top-level element.`, + fix(fixer) { + return fixer.insertTextAfterRange( + jsx.openingElement.typeParameters + ? jsx.openingElement.typeParameters.range + : jsx.openingElement.name.range, + ` data-component="${name}"`, + ); + }, }, }); } } -function handleBlockStatement(context, name, block) { +function handleBlockStatement(context, name, exported, block) { // Find the root return statement. Are there any other types of returns we need to handle? const ret = block.body.find((c) => c.type === 'ReturnStatement'); if (ret && ret.argument.type === 'JSXElement') { - handleJSX(context, name, ret.argument); + handleJSX(context, name, exported, ret.argument); } } @@ -49,31 +53,31 @@ function isForwardRef(expression) { return calleeName === 'forwardRef' && expression.arguments.length == 1; } -function handleExpression(context, name, expression) { +function handleExpression(context, name, exported, expression) { switch (expression.type) { case 'FunctionExpression': - handleBlockStatement(context, name, expression.body); + handleBlockStatement(context, name, exported, expression.body); break; case 'ArrowFunctionExpression': switch (expression.body.type) { case 'JSXElement': - handleJSX(context, name, expression.body); + handleJSX(context, name, exported, expression.body); break; case 'BlockStatement': - handleBlockStatement(context, name, expression.body); + handleBlockStatement(context, name, exported, expression.body); break; } break; case 'ClassExpression': expression.body.body.forEach((x) => { if (x.type === 'MethodDefinition' && x.key.name === 'render') { - handleBlockStatement(context, name, x.value.body); + handleBlockStatement(context, name, exported, x.value.body); } }); break; case 'CallExpression': if (isForwardRef(expression)) { - handleExpression(context, name, expression.arguments[0]); + handleExpression(context, name, exported, expression.arguments[0]); } break; } @@ -94,6 +98,7 @@ const rules = { create(context) { return { Program(root) { + context = { ...context, possibleReports: [] }; root.body.forEach((node) => { // We will need to save any non-exported declarations and handle them only if they get exported at the end let exported = false; @@ -109,11 +114,21 @@ const rules = { switch (node.type) { case 'VariableDeclaration': node.declarations.forEach((variable) => { - handleExpression(context, variable.id.name, variable.init); + handleExpression( + context, + variable.id.name, + exported, + variable.init, + ); }); break; case 'FunctionDeclaration': - handleBlockStatement(context, node.id.name, node.body); + handleBlockStatement( + context, + node.id.name, + exported, + node.body, + ); break; case 'ClassDeclaration': node.body.body.forEach((x) => { @@ -121,7 +136,12 @@ const rules = { x.type === 'MethodDefinition' && x.key.name === 'render' ) { - handleBlockStatement(context, node.id.name, x.value.body); + handleBlockStatement( + context, + node.id.name, + exported, + x.value.body, + ); return; } }); @@ -135,10 +155,31 @@ const rules = { handleExpression( context, node.arguments[0].id.name, + exported, node.arguments[0], ); } break; + case 'AssignmentExpression': + handleExpression(context, node.left.name, exported, node.right); + break; + case 'ExportNamedDeclaration': + node.specifiers.forEach((s) => { + const report = context.possibleReports.find( + (r) => r.name === s.local.name, + ); + if (report) { + report.exported = true; + } + }); + break; + } + }); + + // Report all issues for exported components + context.possibleReports.forEach((r) => { + if (r.exported) { + context.report(r.report); } }); }, diff --git a/test.js b/test.js index 0a2ca96..bcd01b4 100644 --- a/test.js +++ b/test.js @@ -6,11 +6,11 @@ const { join } = require('path'); // Test File Definitions //------------------------------------------------------------------------------ -const singleComponent = `const temp = () => { +const singleComponent = `export const temp = () => { return ; };`; -const singleComponentError = `const temp = () => { +const singleComponentError = `export const temp = () => { return ; };`; @@ -23,20 +23,20 @@ const defaultSingleComponentError = `export default function temp () { };`; const genericTest = ` -const yAxis = (xScale, xTicks) => ( +export const yAxis = (xScale, xTicks) => ( data-component="yAxis" width={1} height={1} xScale={xScale} xTicks={xTicks}> 123 ); `; const genericTestError = ` -const yAxis = (xScale, xTicks) => ( +export const yAxis = (xScale, xTicks) => ( width={1} height={1} xScale={xScale} xTicks={xTicks}> 123 ); `; const renamingComponentDoesntError = ` -const myDiv = () => ( +export const myDiv = () => (
); `; @@ -83,13 +83,15 @@ export const FooChart: React.FC = props => { const multipleComponentsErrors = ` const Component1 = () =>
; const Component2 = () => ; + export { Component1, Component2 } `; const multipleComponents = ` const Component1 = () =>
; const Component2 = () => ; + export { Component1, Component2 } `; -const fragmentsWontUpdate = `const Component = () => +const fragmentsWontUpdate = `export const Component = () => <> @@ -97,19 +99,19 @@ const fragmentsWontUpdate = `const Component = () => ;`; -const classComponent = `class Car extends React.Component { +const classComponent = `export class Car extends React.Component { render() { return

Hi, I am a Car!

; } }`; -const classComponentError = `class Car extends React.Component { +const classComponentError = `export class Car extends React.Component { render() { return

Hi, I am a Car!

; } }`; -const classComponentNestedError = `class Car extends React.Component { +const classComponentNestedError = `export class Car extends React.Component { render() { const Door = () => (

I am a door!

@@ -123,7 +125,7 @@ const classComponentNestedError = `class Car extends React.Component { } }`; -const classComponentNested = `class Car extends React.Component { +const classComponentNested = `export class Car extends React.Component { render() { const Door = () => (

I am a door!

@@ -264,14 +266,22 @@ export const TernaryComponent = () => { }; `; -const ifBlock = /* tsx */ ` -export const IfBlockComponent = () => { - const active = useIsActive(); - if (active) { - return ; - } - return
; -}; +const exportedError = /* tsx */ ` +const NotExported = () => +export const Exported = () => +export default DefaultExported = () => +const ExportedAtEnd = () => +const RenamedExport = () => +export { ExportedAtEnd, RenamedExport as ThisIsRenamed } +`; + +const exported = /* tsx */ ` +const NotExported = () => +export const Exported = () => +export default DefaultExported = () => +const ExportedAtEnd = () => +const RenamedExport = () => +export { ExportedAtEnd, RenamedExport as ThisIsRenamed } `; const tests = { @@ -344,7 +354,7 @@ const tests = { code: ternary, }, { - code: ifBlock, + code: exported, }, ], invalid: [ @@ -427,6 +437,16 @@ const tests = { 'InternalLink is missing the data-component attribute for the top-level element.', ], }, + { + code: exportedError, + output: exported, + errors: [ + 'Exported is missing the data-component attribute for the top-level element.', + 'DefaultExported is missing the data-component attribute for the top-level element.', + 'ExportedAtEnd is missing the data-component attribute for the top-level element.', + 'RenamedExport is missing the data-component attribute for the top-level element.', + ], + }, ], }, },