From 0a8743346a2090eef01f4fbe10105db8398437be Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Fri, 1 Nov 2024 17:28:11 +0100 Subject: [PATCH] default-exports: Refactor rule to handle meta declaration --- docs/rules/default-exports.md | 11 +++++ lib/rules/default-exports.ts | 36 ++++++++++++----- tests/lib/rules/default-exports.test.ts | 54 +++++++++++++++++++++---- 3 files changed, 84 insertions(+), 17 deletions(-) diff --git a/docs/rules/default-exports.md b/docs/rules/default-exports.md index 7de9d6c..67a664c 100644 --- a/docs/rules/default-exports.md +++ b/docs/rules/default-exports.md @@ -19,6 +19,17 @@ export const Primary = {} Examples of **correct** code for this rule: +```js +const meta = { + title: 'Button', + args: { primary: true }, + component: Button, +} +export const meta + +export const Primary = {} +``` + ```js export default { title: 'Button', diff --git a/lib/rules/default-exports.ts b/lib/rules/default-exports.ts index e4c18ba..80e8ab4 100644 --- a/lib/rules/default-exports.ts +++ b/lib/rules/default-exports.ts @@ -34,13 +34,9 @@ export = createStorybookRule({ }, create(context) { - // variables should be defined here - //---------------------------------------------------------------------- // Helpers //---------------------------------------------------------------------- - - // any helper functions should go here or else delete this section const getComponentName = (node: TSESTree.Program, filePath: string) => { const name = path.basename(filePath).split('.')[0] const imported = node.body.find((stmt: TSESTree.Node) => { @@ -62,6 +58,7 @@ export = createStorybookRule({ //---------------------------------------------------------------------- let hasDefaultExport = false + let localMetaNode: TSESTree.Node let hasStoriesOfImport = false return { @@ -70,6 +67,17 @@ export = createStorybookRule({ hasStoriesOfImport = true } }, + VariableDeclaration(node) { + // Take `const meta = {};` into consideration + if ( + node.kind === 'const' && + node.declarations.length === 1 && + node.declarations[0]?.id.type === 'Identifier' && + node.declarations[0]?.id.name === 'meta' + ) { + localMetaNode = node + } + }, ExportDefaultSpecifier: function () { hasDefaultExport = true }, @@ -78,9 +86,9 @@ export = createStorybookRule({ }, 'Program:exit': function (program: TSESTree.Program) { if (!hasDefaultExport && !hasStoriesOfImport) { - const componentName = getComponentName(program, context.getFilename()) + const componentName = getComponentName(program, context.filename) const firstNonImportStatement = program.body.find((n) => !isImportDeclaration(n)) - const node = firstNonImportStatement || program.body[0] || program + const node = firstNonImportStatement ?? program.body[0] ?? program const report = { node, @@ -88,10 +96,18 @@ export = createStorybookRule({ } as const const fix: TSESLint.ReportFixFunction = (fixer) => { - const metaDeclaration = componentName - ? `export default { component: ${componentName} }\n` - : 'export default {}\n' - return fixer.insertTextBefore(node, metaDeclaration) + const sourceCode = context.sourceCode.getText() + // only add semicolons if needed + const semiCharacter = sourceCode.includes(';') ? ';' : '' + if (localMetaNode) { + const exportStatement = `\nexport default meta${semiCharacter}` + return fixer.insertTextAfter(localMetaNode, exportStatement) + } else { + const exportStatement = componentName + ? `export default { component: ${componentName} }${semiCharacter}\n` + : `export default {}${semiCharacter}\n` + return fixer.insertTextBefore(node, exportStatement) + } } context.report({ diff --git a/tests/lib/rules/default-exports.test.ts b/tests/lib/rules/default-exports.test.ts index c1d1c2b..ac42d3e 100644 --- a/tests/lib/rules/default-exports.test.ts +++ b/tests/lib/rules/default-exports.test.ts @@ -48,7 +48,10 @@ ruleTester.run('default-exports', rule, { suggestions: [ { messageId: 'fixSuggestion', - output: 'export default {}\nexport const Primary = () => ', + output: dedent` + export default {} + export const Primary = () => + `, }, ], }, @@ -70,8 +73,11 @@ ruleTester.run('default-exports', rule, { suggestions: [ { messageId: 'fixSuggestion', - output: - "import { MyComponent, Foo } from './MyComponent'\nexport default { component: MyComponent }\nexport const Primary = () => ", + output: dedent` + import { MyComponent, Foo } from './MyComponent' + export default { component: MyComponent } + export const Primary = () => + `, }, ], }, @@ -93,8 +99,11 @@ ruleTester.run('default-exports', rule, { suggestions: [ { messageId: 'fixSuggestion', - output: - "import MyComponent from './MyComponent'\nexport default { component: MyComponent }\nexport const Primary = () => ", + output: dedent` + import MyComponent from './MyComponent' + export default { component: MyComponent } + export const Primary = () => + `, }, ], }, @@ -116,8 +125,39 @@ ruleTester.run('default-exports', rule, { suggestions: [ { messageId: 'fixSuggestion', - output: - "import { MyComponentProps } from './MyComponent'\nexport default {}\nexport const Primary = () => ", + output: dedent` + import { MyComponentProps } from './MyComponent' + export default {} + export const Primary = () => `, + }, + ], + }, + ], + }, + { + code: dedent` + import { MyComponentProps } from './MyComponent'; + export const Primary = () => ; + const meta = { args: { foo: 'bar' } }; + `, + output: dedent` + import { MyComponentProps } from './MyComponent'; + export const Primary = () => ; + const meta = { args: { foo: 'bar' } }; + export default meta; + `, + errors: [ + { + messageId: 'shouldHaveDefaultExport', + suggestions: [ + { + messageId: 'fixSuggestion', + output: dedent` + import { MyComponentProps } from './MyComponent'; + export const Primary = () => ; + const meta = { args: { foo: 'bar' } }; + export default meta; + `, }, ], },