From 7faacea6b9efe68d29ffaca383d09529ff4780fe Mon Sep 17 00:00:00 2001 From: Dmitry Manannikov Date: Mon, 27 May 2019 21:13:34 +0200 Subject: [PATCH] Support extracting JSX component --- dev/playground/a.js | 7 +- dev/playground/t.ts | 1 - .../experimental-use-styled-component.txt | 32 ++++ .../experimental-use-styled-component.ts | 143 +++++++++++++++--- 4 files changed, 158 insertions(+), 25 deletions(-) diff --git a/dev/playground/a.js b/dev/playground/a.js index 25c5951..f00ce32 100644 --- a/dev/playground/a.js +++ b/dev/playground/a.js @@ -1,4 +1,3 @@ -import {get} from 'lodash' -import {Dict} from 'interfaces' -import {Something} from './constants' -import zlib from 'zlib' +const A = () => { + return
hello
+} diff --git a/dev/playground/t.ts b/dev/playground/t.ts index a8c65a5..e69de29 100644 --- a/dev/playground/t.ts +++ b/dev/playground/t.ts @@ -1 +0,0 @@ -const z = (a, b) => {return a + b} diff --git a/src/fixers/__tests__/specs/experimental-use-styled-component.txt b/src/fixers/__tests__/specs/experimental-use-styled-component.txt index 7c7a70e..1d85109 100644 --- a/src/fixers/__tests__/specs/experimental-use-styled-component.txt +++ b/src/fixers/__tests__/specs/experimental-use-styled-component.txt @@ -97,4 +97,36 @@ const A = () => { return r.div([ r(StyledSpan, 'hello') ]) +} +=== +name: extract from selfclosing JSX tag +line: 2 +column: 17 +--- +const A = () => { + return +} +--- +import { styled } from "styletron-react"; + +const StyledImg = styled("img", {}); + +const A = () => { + return +} +=== +name: extract from JSX tag +line: 2 +column: 17 +--- +const A = () => { + return
hello
+} +--- +import { styled } from "styletron-react"; + +const StyledDiv = styled("div", {}); + +const A = () => { + return hello } \ No newline at end of file diff --git a/src/fixers/experimental-use-styled-component.ts b/src/fixers/experimental-use-styled-component.ts index c38b271..0a9fe56 100644 --- a/src/fixers/experimental-use-styled-component.ts +++ b/src/fixers/experimental-use-styled-component.ts @@ -8,8 +8,13 @@ import astTypes from 'recast/lib/types' import {Collection} from 'jscodeshift/src/Collection' import {capitalize} from '../text-utils' import {pathExists} from 'fs-extra' +import {jsxElement} from '@babel/types' -type Data = Position.t +type NodeType = 'jsx' | 'rdom' +type Data = { + start: Position.t + type: NodeType +} function hasStyledImport(j: jscodeshift.JSCodeshift, ast: AstRoot): boolean { const styletronSpecifiers = getImportSpecifiers(j, ast, 'styletron-react') @@ -77,6 +82,10 @@ function getTagName(j: jscodeshift.JSCodeshift, node: jscodeshift.CallExpression : null } +function isLowerCase(word: string): boolean { + return word === word.toLowerCase() +} + const fixer: Fixer = { suggestCodeAction(params) { const {j, ast} = params @@ -95,31 +104,114 @@ const fixer: Fixer = { j.Identifier.check(n.callee.property) ) - if (!node || !node.loc) { + if (node && node.loc) { + const tag = getTagName(j, node) + + return { + title: `Extract "${tag}" to styled component`, + data: {start: node.loc.start, type: 'rdom'}, + } + } + + const jsxElement = Ast.findLastNode( + ast, + j.JSXElement, + n => + n.loc !== null && + Range.isInside(params.selection, n.loc) && + j.JSXIdentifier.check(n.openingElement.name) && + isLowerCase(n.openingElement.name.name) + ) + + if (!jsxElement || !jsxElement.loc) { return null } - const tag = getTagName(j, node) + const jsxNode = jsxElement.openingElement + + if (jsxNode && jsxNode.loc && j.JSXIdentifier.check(jsxNode.name)) { + const tag = jsxNode.name.name - return { - title: `Extract "${tag}" to styled component`, - data: node.loc.start, + return { + title: `Extract "${tag}" to styled component`, + data: {start: jsxElement.loc.start, type: 'jsx'}, + } } + + return null }, createEdit(params) { const {data, ast, j} = params - const node = Ast.findLastNode(ast, j.CallExpression, n => Ast.isOnPosition(n, data)) - if (!node) { - return null + if (data.type === 'rdom') { + const node = Ast.findLastNode(ast, j.CallExpression, n => Ast.isOnPosition(n, data.start)) + if (!node) { + return null + } + + const rDomVar = getDefaultImportName(j, ast, 'r-dom') + if (!rDomVar) { + return null + } + + const tag = getTagName(j, node) + + if (!tag) { + return null + } + + const componentName = 'Styled' + capitalize(tag) + + const patches = [] + + const newNode = j.callExpression(j.identifier(rDomVar), [ + j.identifier(componentName), + ...node.arguments, + ]) + + patches.push(Patch.replaceNode(j, node, newNode)) + + const lastImport = Ast.findLastNode(ast, j.ImportDeclaration, () => true) + const declarationStart = + lastImport && lastImport.loc + ? Position.create(lastImport.loc.end.line, lastImport.loc.end.column) + : Position.create(1, 0) + + const topInserts: any = ['\n'] + + const shouldInsertStyledImport = !hasStyledImport(j, ast) + if (shouldInsertStyledImport) { + const styledImport = j.importDeclaration( + [j.importSpecifier(j.identifier('styled'))], + j.literal('styletron-react') + ) + topInserts.push(styledImport) + topInserts.push('\n') + } + + topInserts.push('\n') + + const styledComponentDeclaration = j.variableDeclaration('const', [ + j.variableDeclarator( + j.identifier(componentName), + j.callExpression(j.identifier('styled'), [j.literal(tag), j.objectExpression([])]) + ), + ]) + + topInserts.push(styledComponentDeclaration) + + patches.push(Patch.insert(j, declarationStart, topInserts)) + + return patches } - const rDomVar = getDefaultImportName(j, ast, 'r-dom') - if (!rDomVar) { + const elementNode = Ast.findLastNode(ast, j.JSXElement, n => Ast.isOnPosition(n, data.start)) + if (!elementNode) { return null } + const node = elementNode.openingElement - const tag = getTagName(j, node) + const tag = j.JSXIdentifier.check(node.name) && node.name.name if (!tag) { return null @@ -129,10 +221,21 @@ const fixer: Fixer = { const patches = [] - const newNode = j.callExpression(j.identifier(rDomVar), [ - j.identifier(componentName), - ...node.arguments, - ]) + const newNode = Ast.cloneNode(j, node) + newNode.name = j.jsxIdentifier(componentName) + newNode.attributes = newNode.attributes.filter( + attr => + !( + j.JSXAttribute.check(attr) && + j.JSXIdentifier.check(attr.name) && + attr.name.name === 'style' + ) + ) + if (!node.selfClosing && elementNode.closingElement) { + const newNode = Ast.cloneNode(j, elementNode.closingElement) + newNode.name = j.jsxIdentifier(componentName) + patches.push(Patch.replaceNode(j, elementNode.closingElement, newNode)) + } patches.push(Patch.replaceNode(j, node, newNode)) @@ -142,7 +245,7 @@ const fixer: Fixer = { ? Position.create(lastImport.loc.end.line, lastImport.loc.end.column) : Position.create(1, 0) - const topInserts: any = ['\n'] + const topInserts: any = lastImport ? ['\n'] : [] const shouldInsertStyledImport = !hasStyledImport(j, ast) if (shouldInsertStyledImport) { @@ -165,10 +268,10 @@ const fixer: Fixer = { topInserts.push(styledComponentDeclaration) + topInserts.push('\n') + topInserts.push('\n') - patches.push( - Patch.insert(j, declarationStart, topInserts) - ) + patches.push(Patch.insert(j, declarationStart, topInserts)) return patches },