Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSX transforms #3

Merged
merged 8 commits into from
Aug 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ 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`**

Tells the plugin 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 be 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
10 changes: 10 additions & 0 deletions __tests__/__fixtures__/with-babel-plugin/01-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"/>;
27 changes: 27 additions & 0 deletions __tests__/__fixtures__/with-babel-plugin/01-jsx/output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const GDot1 = props => <span {...props}>Hi, I'm a Span!</span>;

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

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

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

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)} />;
16 changes: 14 additions & 2 deletions __tests__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import pluginTester from "babel-plugin-tester";
import glamorousToEmotion from "../index.js";
import path from "path";

pluginTester({
const sharedOptions = {
plugin: glamorousToEmotion,
babelOptions: {
// taken from https://github.com/square/babel-codemod/blob/00ae5984e1b2ca2fac923011ce16157a29b12b39/src/AllSyntaxPlugin.ts
Expand All @@ -28,5 +28,17 @@ pluginTester({
babelrc: false,
compact: false,
},
fixtures: path.join(__dirname, "__fixtures__"),
};

pluginTester({
...sharedOptions,
fixtures: path.join(__dirname, "__fixtures__", "without-babel-plugin"),
});

pluginTester({
...sharedOptions,
fixtures: path.join(__dirname, "__fixtures__", "with-babel-plugin"),
pluginOptions: {
withBabelPlugin: true,
},
});
167 changes: 137 additions & 30 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
* https://astexplorer.net/#/gist/7bc4771564a12c9f93c4904b3934aa1c/latests
*/

const htmlElementAttributes = require("react-html-attributes");

module.exports = function(babel) {
const {types: t} = babel;

Expand All @@ -39,16 +41,93 @@ module.exports = function(babel) {
});
};

// transform <glamorous.Div css={styles} width={100}/> to <div css={{...styles, width: 100}}/>
const transformJSXAttributes = ({tagName, jsxAttrs, withBabelPlugin, getCssFn, getCxFn}) => {
if (!jsxAttrs) return [];
const stylesArguments = [];
let classNameAttr = null;
let originalCssValue;

/*
We go through all jsx attributes and filter out all style-specific props. E.g `css` or `marginTop`.
All style-specific props are gathered within `stylesArguments` and processed below
*/
const transformedJsxAttrs = jsxAttrs.filter(attr => {
if (t.isJSXSpreadAttribute(attr)) return true;
const {value, name: jsxKey} = attr;
if (jsxKey.name === "css") {
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;

// ignore generic attributes like 'id'
if (htmlElementAttributes["*"].includes(jsxKey.name)) return true;

// ignore tag specific attrs like 'disabled'
const tagSpecificAttrs = htmlElementAttributes[tagName];
if (tagSpecificAttrs && tagSpecificAttrs.includes(jsxKey.name)) return true;

stylesArguments.push(
t.objectProperty(
t.identifier(jsxKey.name),
t.isJSXExpressionContainer(value) ? value.expression : value
)
);
return false;
}
return true;
});

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
transformedJsxAttrs.push(
t.jsxAttribute(t.jsxIdentifier("css"), t.jsxExpressionContainer(stylesObject))
);
} else {
// if babel plugin is not enabled use <div className={css(styles)}/> syntax

if (!classNameAttr) {
const cssCall = t.callExpression(getCssFn(), [stylesObject]);
transformedJsxAttrs.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);
}
}
}
return transformedJsxAttrs;
};

const glamorousVisitor = {
// for each reference to an identifier...
ReferencedIdentifier(path, {getNewName, oldName}) {
ReferencedIdentifier(path, {getStyledFn, 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();
path.replaceWith(getStyledFn());
break;
}

Expand All @@ -58,7 +137,7 @@ module.exports = function(babel) {
if (t.isCallExpression(grandParentPath.node)) {
grandParentPath.replaceWith(
t.callExpression(
t.callExpression(t.identifier(getNewName()), [
t.callExpression(getStyledFn(), [
t.stringLiteral(grandParentPath.node.callee.property.name),
]),
fixContentProp(grandParentPath.node.arguments)
Expand All @@ -75,11 +154,16 @@ module.exports = function(babel) {
// replace <glamorous.Div/> with `<div/>`
case "JSXMemberExpression": {
const grandParent = path.parentPath.parent;
grandParent.name = t.identifier(grandParent.name.property.name.toLowerCase());
if (t.isJSXOpeningElement(grandParent) && grandParent.attributes) {
console.warn(
"The current version of the codemod has only weak support of the '<glamorous.Div>' syntax. It won't transform any attributes."
);
const tagName = grandParent.name.property.name.toLowerCase();
grandParent.name = t.identifier(tagName);
if (t.isJSXOpeningElement(grandParent)) {
grandParent.attributes = transformJSXAttributes({
tagName,
jsxAttrs: grandParent.attributes,
withBabelPlugin,
getCssFn,
getCxFn,
});
}
break;
}
Expand All @@ -100,47 +184,70 @@ module.exports = function(babel) {
return;
}

// 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";
let newImports = [];
let useDefaultImport = false;

// 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;
// use "name" as identifier, but only if it's not already used in the current scope
const createUniqueIdentifier = name =>
path.scope.hasBinding(name) ? path.scope.generateUidIdentifier(name) : t.identifier(name);

// this object collects all the imports we'll need from "react-emotion"
let emotionImports = {};

const getStyledFn = () => {
if (!emotionImports["default"]) {
emotionImports["default"] = t.importDefaultSpecifier(createUniqueIdentifier("styled"));
}
return emotionImports["default"].local;
};

const getCssFn = () => {
if (!emotionImports["css"]) {
const specifier = t.importSpecifier(t.identifier("css"), createUniqueIdentifier("css"));
emotionImports["css"] = specifier;
}
return newName;
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, {
getStyledFn,
oldName: s.local.name,
withBabelPlugin: opts.withBabelPlugin,
getCssFn,
getCxFn,
});
});

const themeProvider = path.node.specifiers.find(
specifier => specifier.local.name === "ThemeProvider"
);

if (themeProvider) {
newImports.push(
path.insertBefore(
t.importDeclaration(
[t.importSpecifier(t.identifier("ThemeProvider"), t.identifier("ThemeProvider"))],
t.stringLiteral("emotion-theming")
)
);
}

newImports.forEach(ni => path.insertBefore(ni));
// if we needed something from the emotion lib, we need to add the import
if (Object.keys(emotionImports).length) {
path.insertBefore(
t.importDeclaration(
Object.values(emotionImports),
t.stringLiteral(opts.preact ? "preact-emotion" : "react-emotion")
)
);
}

path.remove();
},
},
Expand Down
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,8 @@
"babel-preset-react": "^7.0.0-beta.3",
"eslint": "4.x",
"jest": "^23.4.2"
},
"dependencies": {
"react-html-attributes": "^1.4.3"
}
}