diff --git a/README.md b/README.md index bc8779f..12306ae 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,18 @@ You'll need to use [`babel-codemod`](https://github.com/square/babel-codemod) to This will put you fully in emotion-land. +### Options + +you may also pass a `--plugin-options` argument to the `babel-codemod` command. Here are the available options: + +- **withBabelPlugin=true** + assumes that your emotion setup includes the [emotion-babel-plugin](https://github.com/emotion-js/emotion/tree/master/packages/babel-plugin-emotion). Without this option, `` gets translated to `
`. + If this option is enabled, it will transformed to `
`. + +- **preact=true** + Uses `import styled from preact-emotion` instead of `import styled from react-emotion` + + ## Contributing ### ALL CONTRIBUTIONS WELCOME! I sincerely hope it helps you migrate your codebase! Please open issues for areas where it doesn't quite help and we'll sort it out. diff --git a/__tests__/__fixtures__/02-jsx/code.js b/__tests__/__fixtures__/with-babel-plugin/01-jsx/code.js similarity index 74% rename from __tests__/__fixtures__/02-jsx/code.js rename to __tests__/__fixtures__/with-babel-plugin/01-jsx/code.js index 2095068..238993e 100644 --- a/__tests__/__fixtures__/02-jsx/code.js +++ b/__tests__/__fixtures__/with-babel-plugin/01-jsx/code.js @@ -6,3 +6,5 @@ const GDot3 = props => ; const GDot4 = props => ; const GDot5 = props => ; const GDot6 = props => ; +const GDot7 = props => ; +const GDot8 = props => ; diff --git a/__tests__/__fixtures__/02-jsx/output.js b/__tests__/__fixtures__/with-babel-plugin/01-jsx/output.js similarity index 62% rename from __tests__/__fixtures__/02-jsx/output.js rename to __tests__/__fixtures__/with-babel-plugin/01-jsx/output.js index 328ed3c..f9fdfb5 100644 --- a/__tests__/__fixtures__/02-jsx/output.js +++ b/__tests__/__fixtures__/with-babel-plugin/01-jsx/output.js @@ -8,14 +8,20 @@ const GDot3 = props =>
; -const GDot4 = props =>
; +}} />; const GDot5 = props => ; const GDot6 = props => ; + +const GDot7 = props => ; + +const GDot8 = props => ; diff --git a/__tests__/__fixtures__/01-styled-already-present/code.js b/__tests__/__fixtures__/without-babel-plugin/01-styled-already-present/code.js similarity index 100% rename from __tests__/__fixtures__/01-styled-already-present/code.js rename to __tests__/__fixtures__/without-babel-plugin/01-styled-already-present/code.js diff --git a/__tests__/__fixtures__/01-styled-already-present/output.js b/__tests__/__fixtures__/without-babel-plugin/01-styled-already-present/output.js similarity index 100% rename from __tests__/__fixtures__/01-styled-already-present/output.js rename to __tests__/__fixtures__/without-babel-plugin/01-styled-already-present/output.js diff --git a/__tests__/__fixtures__/without-babel-plugin/02-jsx/code.js b/__tests__/__fixtures__/without-babel-plugin/02-jsx/code.js new file mode 100644 index 0000000..238993e --- /dev/null +++ b/__tests__/__fixtures__/without-babel-plugin/02-jsx/code.js @@ -0,0 +1,10 @@ +import g from "glamorous"; + +const GDot1 = props => Hi, I'm a Span!; +const GDot2 = props => ; +const GDot3 = props => ; +const GDot4 = props => ; +const GDot5 = props => ; +const GDot6 = props => ; +const GDot7 = props => ; +const GDot8 = props => ; diff --git a/__tests__/__fixtures__/without-babel-plugin/02-jsx/output.js b/__tests__/__fixtures__/without-babel-plugin/02-jsx/output.js new file mode 100644 index 0000000..ee5c2bc --- /dev/null +++ b/__tests__/__fixtures__/without-babel-plugin/02-jsx/output.js @@ -0,0 +1,29 @@ +import { css, cx } from "react-emotion"; + +const GDot1 = props => Hi, I'm a Span!; + +const GDot2 = props =>
; + +const GDot3 = props =>
; + +const GDot4 = props =>
; + +const GDot5 = props => ; + +const GDot6 = props => ; + +const GDot7 = props => ; + +const GDot8 = props => ; diff --git a/__tests__/__fixtures__/03-factories/code.js b/__tests__/__fixtures__/without-babel-plugin/03-factories/code.js similarity index 100% rename from __tests__/__fixtures__/03-factories/code.js rename to __tests__/__fixtures__/without-babel-plugin/03-factories/code.js diff --git a/__tests__/__fixtures__/03-factories/output.js b/__tests__/__fixtures__/without-babel-plugin/03-factories/output.js similarity index 100% rename from __tests__/__fixtures__/03-factories/output.js rename to __tests__/__fixtures__/without-babel-plugin/03-factories/output.js diff --git a/__tests__/__fixtures__/04-theme provider/code.js b/__tests__/__fixtures__/without-babel-plugin/04-theme provider/code.js similarity index 100% rename from __tests__/__fixtures__/04-theme provider/code.js rename to __tests__/__fixtures__/without-babel-plugin/04-theme provider/code.js diff --git a/__tests__/__fixtures__/04-theme provider/output.js b/__tests__/__fixtures__/without-babel-plugin/04-theme provider/output.js similarity index 100% rename from __tests__/__fixtures__/04-theme provider/output.js rename to __tests__/__fixtures__/without-babel-plugin/04-theme provider/output.js diff --git a/__tests__/index.js b/__tests__/index.js index 52f54aa..3532b7c 100644 --- a/__tests__/index.js +++ b/__tests__/index.js @@ -28,5 +28,37 @@ pluginTester({ babelrc: false, compact: false, }, - fixtures: path.join(__dirname, "__fixtures__"), + fixtures: path.join(__dirname, "__fixtures__", "without-babel-plugin"), +}); + +pluginTester({ + plugin: glamorousToEmotion, + babelOptions: { + // taken from https://github.com/square/babel-codemod/blob/00ae5984e1b2ca2fac923011ce16157a29b12b39/src/AllSyntaxPlugin.ts + parserOpts: { + sourceType: "module", + allowImportExportEverywhere: true, + allowReturnOutsideFunction: true, + allowSuperOutsideMethod: true, + ranges: false, + plugins: [ + "jsx", + "asyncGenerators", + "classProperties", + "doExpressions", + "exportExtensions", + "functionBind", + "functionSent", + "objectRestSpread", + "dynamicImport", + "decorators", + ], + }, + babelrc: false, + compact: false, + }, + fixtures: path.join(__dirname, "__fixtures__", "with-babel-plugin"), + pluginOptions: { + withBabelPlugin: true, + }, }); diff --git a/index.js b/index.js index 8393838..80400f4 100644 --- a/index.js +++ b/index.js @@ -41,15 +41,26 @@ module.exports = function(babel) { }); }; - const transformJSXAttributes = (tagName, jsxAttrs) => { + // transform to
+ const transformJSXAttributes = ({tagName, jsxAttrs, withBabelPlugin, getCssFn, getCxFn}) => { if (!jsxAttrs) return []; - const appendToCss = []; - let cssAttr = null; + const stylesArguments = []; + let classNameAttr = null; + let originalCssValue; const newAttrs = jsxAttrs.filter(attr => { if (t.isJSXSpreadAttribute(attr)) return true; const {value, name: jsxKey} = attr; if (jsxKey.name === "css") { - cssAttr = attr; + originalCssValue = value.expression; + // move properties of css attribute to the very front via unshift + if (!t.isObjectExpression(value.expression)) { + stylesArguments.unshift(t.spreadElement(value.expression)); + } else { + stylesArguments.unshift(...value.expression.properties); + } + return false; + } else if (jsxKey.name === "className") { + classNameAttr = attr; } else { // ignore event handlers if (jsxKey.name.match(/on[A-Z]/)) return true; @@ -61,44 +72,58 @@ module.exports = function(babel) { const tagSpecificAttrs = htmlElementAttributes[tagName]; if (tagSpecificAttrs && tagSpecificAttrs.includes(jsxKey.name)) return true; - appendToCss.push({ - name: t.identifier(jsxKey.name), - value: t.isJSXExpressionContainer(value) ? value.expression : value, - }); + stylesArguments.push( + t.objectProperty( + t.identifier(jsxKey.name), + t.isJSXExpressionContainer(value) ? value.expression : value + ) + ); return false; } return true; }); - if (appendToCss.length > 0) { - if (!cssAttr) { - cssAttr = t.jsxAttribute( - t.jsxIdentifier("css"), - t.jsxExpressionContainer(t.objectExpression([])) + + if (stylesArguments.length > 0) { + // if the css property was the only object, we don't need to use it's spreaded version + const stylesObject = + originalCssValue && stylesArguments.length === 1 + ? originalCssValue + : t.objectExpression(stylesArguments); + + if (withBabelPlugin) { + // if babel plugin is enabled use
syntax + newAttrs.push( + t.jsxAttribute(t.jsxIdentifier("css"), t.jsxExpressionContainer(stylesObject)) ); - newAttrs.push(cssAttr); - } else if (!t.isObjectExpression(cssAttr.value.expression)) { - // turn into - // so we can add more properties to this css attribute - cssAttr.value.expression = t.objectExpression([t.spreadElement(cssAttr.value.expression)]); + } else { + // if babel plugin is not enabled use
syntax + + if (!classNameAttr) { + const cssCall = t.callExpression(getCssFn(), [stylesObject]); + newAttrs.push( + t.jsxAttribute(t.jsxIdentifier("className"), t.jsxExpressionContainer(cssCall)) + ); + } else { + // if className is already present use
syntax + const cxCall = t.callExpression(getCxFn(), [classNameAttr.value, stylesObject]); + classNameAttr.value = t.jsxExpressionContainer(cxCall); + } } - appendToCss.forEach(({name, value}) => { - cssAttr.value.expression.properties.push(t.objectProperty(name, value)); - }); - cssAttr.value.expression.properties; } return newAttrs; }; const glamorousVisitor = { // for each reference to an identifier... - ReferencedIdentifier(path, {getNewName, oldName}) { + ReferencedIdentifier(path, {getNewName, oldName, withBabelPlugin, getCssFn, getCxFn}) { // skip if the name of the identifier does not correspond to the name of glamorous default import if (path.node.name !== oldName) return; switch (path.parent.type) { // replace `glamorous()` with `styled()` case "CallExpression": { - path.node.name = getNewName(); + console.log("path.node", path.node); + path.replaceWith(getNewName()); break; } @@ -108,7 +133,7 @@ module.exports = function(babel) { if (t.isCallExpression(grandParentPath.node)) { grandParentPath.replaceWith( t.callExpression( - t.callExpression(t.identifier(getNewName()), [ + t.callExpression(getNewName(), [ t.stringLiteral(grandParentPath.node.callee.property.name), ]), fixContentProp(grandParentPath.node.arguments) @@ -128,7 +153,13 @@ module.exports = function(babel) { const tagName = grandParent.name.property.name.toLowerCase(); grandParent.name = t.identifier(tagName); if (t.isJSXOpeningElement(grandParent)) { - grandParent.attributes = transformJSXAttributes(tagName, grandParent.attributes); + grandParent.attributes = transformJSXAttributes({ + tagName, + jsxAttrs: grandParent.attributes, + withBabelPlugin, + getCssFn, + getCxFn, + }); } break; } @@ -150,30 +181,45 @@ module.exports = function(babel) { } // use "styled" as new default import, only if there's no such variable in use yet - const newName = path.scope.hasBinding("styled") - ? path.scope.generateUidIdentifier("styled").name - : "styled"; + const createUniqueIdentifier = name => + path.scope.hasBinding(name) ? path.scope.generateUidIdentifier(name) : t.identifier(name); let newImports = []; - let useDefaultImport = false; + let emotionImports = {}; // only if the traversal below wants to know the newName, // we're gonna add the default import const getNewName = () => { - if (!useDefaultImport) { - newImports.push( - t.importDeclaration( - [t.importDefaultSpecifier(t.identifier(newName))], - t.stringLiteral(opts.preact ? "preact-emotion" : "react-emotion") - ) - ); - useDefaultImport = true; + if (!emotionImports["default"]) { + emotionImports["default"] = t.importDefaultSpecifier(createUniqueIdentifier("styled")); } - return newName; + return emotionImports["default"].local; + }; + + const getCssFn = () => { + if (!emotionImports["css"]) { + const specifier = t.importSpecifier(t.identifier("css"), createUniqueIdentifier("css")); + emotionImports["css"] = specifier; + } + return emotionImports["css"].local; + }; + + const getCxFn = () => { + if (!emotionImports["cx"]) { + const specifier = t.importSpecifier(t.identifier("cx"), createUniqueIdentifier("cx")); + emotionImports["cx"] = specifier; + } + return emotionImports["cx"].local; }; // only if the default import of glamorous is used, we're gonna apply the transforms path.node.specifiers.filter(s => t.isImportDefaultSpecifier(s)).forEach(s => { - path.parentPath.traverse(glamorousVisitor, {getNewName, oldName: s.local.name}); + path.parentPath.traverse(glamorousVisitor, { + getNewName, + oldName: s.local.name, + withBabelPlugin: opts.withBabelPlugin, + getCssFn, + getCxFn, + }); }); const themeProvider = path.node.specifiers.find( @@ -189,6 +235,16 @@ module.exports = function(babel) { ); } + // if we needed something from the emotion lib, we need to add the import + if (Object.keys(emotionImports).length) { + newImports.push( + t.importDeclaration( + Object.values(emotionImports), + t.stringLiteral(opts.preact ? "preact-emotion" : "react-emotion") + ) + ); + } + newImports.forEach(ni => path.insertBefore(ni)); path.remove(); },