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

Rewrite of the codemod #2

Merged
merged 7 commits into from
Aug 6, 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
3 changes: 3 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": ["env", "react"]
}
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__fixtures__
8 changes: 8 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"parser": "babel-eslint",
"env": {"node": true},
"extends": "eslint:recommended",
"rules": {
"no-console": 0
}
}
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__fixtures__
5 changes: 5 additions & 0 deletions __tests__/__fixtures__/01-styled-already-present/code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import glamorous from "glamorous";

const styled = "already present";

const ComponentReference = glamorous(MyComponent);
4 changes: 4 additions & 0 deletions __tests__/__fixtures__/01-styled-already-present/output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import _styled from "react-emotion";
const styled = "already present";

const ComponentReference = _styled(MyComponent);
8 changes: 8 additions & 0 deletions __tests__/__fixtures__/03-factories/code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import glamorous from "glamorous";

const CommonCase = glamorous.div({
display: "none",
content: "",
fontFamily: '"Arial", Helvetica, sans-serif',
fontSize: 20
});
7 changes: 7 additions & 0 deletions __tests__/__fixtures__/03-factories/output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import styled from "react-emotion";
const CommonCase = styled("div")({
display: "none",
content: "\"\"",
fontFamily: '"Arial", Helvetica, sans-serif',
fontSize: 20
});
1 change: 1 addition & 0 deletions __tests__/__fixtures__/04-theme provider/code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import g, { ThemeProvider } from "glamorous";
1 change: 1 addition & 0 deletions __tests__/__fixtures__/04-theme provider/output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import { ThemeProvider } from "emotion-theming";
32 changes: 32 additions & 0 deletions __tests__/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import pluginTester from "babel-plugin-tester";
import glamorousToEmotion from "../index.js";
import path from "path";

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__"),
});
231 changes: 117 additions & 114 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,133 +13,136 @@
* babel cli:
*
* babel [your-source-dir] --plugins=glamorous-to-emotion --presets=react,etc... --out-dir=[your-source-dir]
*
*
* A demo can be seen at:
* https://astexplorer.net/#/gist/7bc4771564a12c9f93c4904b3934aa1c/latests
*/

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

return {
visitor: {
/**
* Find all JSX elements that use the old
* <glamorous.Something> notation and rewrite them to be
* plain ole somethings.
*/
JSXElement(path) {
path.traverse({
"JSXOpeningElement|JSXClosingElement"(path) {
if (!t.isJSXMemberExpression(path.node.name)) {
return;
}
if (path.node.name.object.name !== "glamorous") {
return;
}
path.node.name = t.identifier(
path.node.name.property.name.toLowerCase()
);
}
});
},
// glamorous and emotion treat the css attribute "content" differently.
// we need to put it's content inside a string.
// i.e. turn {content: ""} into {content: '""'}
const fixContentProp = glamorousFactoryArguments => {
return glamorousFactoryArguments.map(arg => {
if (t.isObjectExpression(arg)) {
arg.properties = arg.properties.map(
prop =>
prop.key.name === "content"
? {...prop, value: t.stringLiteral(`"${prop.value.value}"`)}
: prop
);
}
// TODO: if `arg` is a function, we might want to inspect its return value
return arg;
});
};

/**
* Find glamorous import statements, including ThemeProvider
* imports and rewrite them to be emotion imports.
*/
ImportDeclaration(path, { opts }) {
if (path.node.source.value !== "glamorous") {
return;
}
const glamorousVisitor = {
// for each reference to an identifier...
ReferencedIdentifier(path, {getNewName, oldName}) {
// skip if the name of the identifier does not correspond to the name of glamorous default import
if (path.node.name !== oldName) return;

// First, check for ThemeProvider and update that.
const themeProviderIndex = path.node.specifiers.findIndex(
specifier => specifier.local.name === "ThemeProvider"
);
switch (path.parent.type) {
// replace `glamorous()` with `styled()`
case "CallExpression": {
path.node.name = getNewName();
break;
}

if (~themeProviderIndex) {
path.insertAfter(
t.importDeclaration(
[
t.importSpecifier(
t.identifier("ThemeProvider"),
t.identifier("ThemeProvider")
)
],
t.stringLiteral("emotion-theming")
)
);
}
// replace `glamorous.div()` with `styled("div")()`
case "MemberExpression": {
const grandParentPath = path.parentPath.parentPath;
if (t.isCallExpression(grandParentPath.node)) {
grandParentPath.replaceWith(
t.callExpression(
t.callExpression(t.identifier(getNewName()), [
t.stringLiteral(grandParentPath.node.callee.property.name),
]),
fixContentProp(grandParentPath.node.arguments)
)
);
} else {
throw new Error(
`Not sure how to deal with glamorous within MemberExpression @ ${path.node.loc}`
);
}
break;
}

// Then, replace the whole path with an emotion one!
path.replaceWith(
t.importDeclaration(
[t.importDefaultSpecifier(t.identifier("styled"))],
t.stringLiteral(opts.preact ? "preact-emotion" : "react-emotion")
)
);
},
// 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."
);
}
break;
}

/**
* Lastly, find all glamorous.Something() calls
* and replace them with emotion('something') calls.
*/
CallExpression(path) {
default: {
console.warning("Found glamorous being used in an unkonwn context:", path.parent.type);
}
}
},
};

/**
* First, rewrite all glamorous(Something) calls.
*/
if (
t.isIdentifier(path.node.callee) &&
path.node.callee.name === "glamorous"
) {
path.node.callee.name = "styled";
return;
}
return {
name: "glamorousToEmotion",
visitor: {
ImportDeclaration(path, {opts}) {
const {value: libName} = path.node.source;
if (libName !== "glamorous" && libName !== "glamorous.macro") {
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;

/**
* Now, rewrite all glamorous.div(someStyle) calls.
*/
if (!t.isMemberExpression(path.node.callee)) {
return;
}
if (path.node.callee.object.name !== "glamorous") {
return;
}
path.replaceWith(
t.callExpression(
t.callExpression(t.identifier("styled"), [
t.stringLiteral(path.node.callee.property.name)
]),
// only if the traversal below wants to know the newName,
// we're gonna add the default import
const getNewName = () => {
Copy link
Owner

Choose a reason for hiding this comment

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

How does this work on line 61 if it's defined here? 🤔 Surely I've missed something.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Whatever you put as a second argument when calling a traverse step, will be available as state in the second parameter of each Visitor.

I pass the getNewName parameter here

Check this part of kentcdodds best practices for more information!

Copy link
Owner

Choose a reason for hiding this comment

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

Ah! I missed the part where it was being passed in on line 44! Makes sense!

if (!useDefaultImport) {
newImports.push(
t.importDeclaration(
[t.importDefaultSpecifier(t.identifier(newName))],
t.stringLiteral(opts.preact ? "preact-emotion" : "react-emotion")
)
);
useDefaultImport = true;
}
return newName;
};

/**
* Map over the arguments for an ObjectExpression,
* usually the styles, and look for the 'content'
* property.
*/
path.node.arguments.map(argument => {
if (argument.type !== "ObjectExpression") {
return argument;
}
return {
...argument,
properties: argument.properties.map(prop => {
if (prop.key.name !== "content") {
return prop;
}
// 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});
});

// Add quotes to the content property.
return {
...prop,
value: t.stringLiteral(`"${prop.value.value}"`)
};
})
};
})
)
);
}
const themeProvider = path.node.specifiers.find(
specifier => specifier.local.name === "ThemeProvider"
);

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

newImports.forEach(ni => path.insertBefore(ni));
path.remove();
},
},
};
};
Loading