Skip to content

Commit

Permalink
add withBabelPlugin option
Browse files Browse the repository at this point in the history
  • Loading branch information
danielberndt committed Aug 6, 2018
1 parent ad684af commit 0d90bcf
Show file tree
Hide file tree
Showing 13 changed files with 190 additions and 43 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, `<glamorous.Div marginTop={5}/>` gets translated to `<div className={css({marginTop: 5})}>`.
If this option is enabled, it will transformed to `<div css={{marginTop: 5}}>`.

- **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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ const GDot3 = props => <g.Div marginTop={5}/>;
const GDot4 = props => <g.Div marginTop={5} css={{marginTop: 10}} marginBottom="5" onClick={handler}/>;
const GDot5 = props => <g.Img width={100}/>;
const GDot6 = props => <g.Span width={100}/>;
const GDot7 = props => <g.Span css={redStyles} marginLeft={5}/>;
const GDot8 = props => <g.Span css={redStyles} className="my-class"/>;
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,20 @@ const GDot3 = props => <div css={{
marginTop: 5
}} />;

const GDot4 = props => <div css={{
const GDot4 = props => <div onClick={handler} css={{
marginTop: 10,
marginTop: 5,
marginBottom: "5"
}} onClick={handler} />;
}} />;

const GDot5 = props => <img width={100} />;

const GDot6 = props => <span css={{
width: 100
}} />;

const GDot7 = props => <span css={{ ...redStyles,
marginLeft: 5
}} />;

const GDot8 = props => <span className="my-class" css={redStyles} />;
10 changes: 10 additions & 0 deletions __tests__/__fixtures__/without-babel-plugin/02-jsx/code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import g from "glamorous";

const GDot1 = props => <g.Span {...props}>Hi, I'm a Span!</g.Span>;
const GDot2 = props => <g.Div css={{marginTop: 5}}/>;
const GDot3 = props => <g.Div marginTop={5}/>;
const GDot4 = props => <g.Div marginTop={5} css={{marginTop: 10}} marginBottom="5" onClick={handler}/>;
const GDot5 = props => <g.Img width={100}/>;
const GDot6 = props => <g.Span width={100}/>;
const GDot7 = props => <g.Span css={redStyles} marginLeft={5}/>;
const GDot8 = props => <g.Span css={redStyles} className="my-class"/>;
29 changes: 29 additions & 0 deletions __tests__/__fixtures__/without-babel-plugin/02-jsx/output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { css, cx } from "react-emotion";

const GDot1 = props => <span {...props}>Hi, I'm a Span!</span>;

const GDot2 = props => <div className={css({
marginTop: 5
})} />;

const GDot3 = props => <div className={css({
marginTop: 5
})} />;

const GDot4 = props => <div onClick={handler} className={css({
marginTop: 10,
marginTop: 5,
marginBottom: "5"
})} />;

const GDot5 = props => <img width={100} />;

const GDot6 = props => <span className={css({
width: 100
})} />;

const GDot7 = props => <span className={css({ ...redStyles,
marginLeft: 5
})} />;

const GDot8 = props => <span className={cx("my-class", redStyles)} />;
File renamed without changes.
34 changes: 33 additions & 1 deletion __tests__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
});
136 changes: 96 additions & 40 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,26 @@ module.exports = function(babel) {
});
};

const transformJSXAttributes = (tagName, jsxAttrs) => {
// transform <glamorous.Div css={styles} width={100}/> to <div css={{...styles, width: 100}}/>
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;
Expand All @@ -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 <div css={styles}/> syntax
newAttrs.push(
t.jsxAttribute(t.jsxIdentifier("css"), t.jsxExpressionContainer(stylesObject))
);
newAttrs.push(cssAttr);
} else if (!t.isObjectExpression(cssAttr.value.expression)) {
// turn <span css={obj} .../> into <span css={{...obj}} .../>
// 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 <div className={css(styles)}/> 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 <div className={cx("my-className", styles)}/> 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;
}

Expand All @@ -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)
Expand All @@ -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;
}
Expand All @@ -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(
Expand All @@ -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();
},
Expand Down

0 comments on commit 0d90bcf

Please sign in to comment.