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 7 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:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-you
+You


- **`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})}>`.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-assumes
+Tells the plugin


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`
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-from preact-emotion
+from "preact-emotion"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all done!



## 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 => {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry to be a pain! But transformedJsxAttrs => transformed from what? A little

/**
  * this does this
  */

would be helpful

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then we can merge it 🚀

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no worries :) Update is in.

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 = {};
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this do?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on what features are needed, we potentially need to import the default export or css or cx from react-emotion.
These imports are added to the code here


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"
}
}