-
Notifications
You must be signed in to change notification settings - Fork 2
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
Changes from 6 commits
9685858
ad684af
0d90bcf
1d92475
899ec97
2631f87
59e1707
e611f31
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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`** | ||
|
||
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})}>`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. -from preact-emotion
+from "preact-emotion" There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
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"/>; |
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} />; |
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"/>; |
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)} />; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,8 @@ | |
* https://astexplorer.net/#/gist/7bc4771564a12c9f93c4904b3934aa1c/latests | ||
*/ | ||
|
||
const htmlElementAttributes = require("react-html-attributes"); | ||
|
||
module.exports = function(babel) { | ||
const {types: t} = babel; | ||
|
||
|
@@ -39,16 +41,88 @@ 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; | ||
const transformedJsxAttrs = jsxAttrs.filter(attr => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry to be a pain! But
would be helpful There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Then we can merge it 🚀 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
|
||
|
@@ -58,7 +132,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) | ||
|
@@ -75,11 +149,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; | ||
} | ||
|
@@ -100,47 +179,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 = {}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does this do? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Depending on what features are needed, we potentially need to import the |
||
|
||
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(); | ||
}, | ||
}, | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.