Skip to content

Instantly share code, notes, and snippets.

@gimenete
Created April 13, 2023 22:03
Show Gist options
  • Save gimenete/af8f5121b278d9c0486a8a24d1684240 to your computer and use it in GitHub Desktop.
Save gimenete/af8f5121b278d9c0486a8a24d1684240 to your computer and use it in GitHub Desktop.
Twin macro codemod
import { NodePath } from "@babel/traverse";
import {
Expression,
JSXOpeningElement,
SpreadElement,
TaggedTemplateExpression,
StringLiteral,
Identifier,
ImportDeclaration,
} from "@babel/types";
import { defineCodemod } from "@codemod/cli";
export default defineCodemod(({ t, m }) => {
const twimImport = m.importDeclaration(
undefined,
m.stringLiteral("twin.macro"),
);
const twimTemplate = m.taggedTemplateExpression(m.identifier("tw"));
const twStyle = m.identifier("TwStyle");
const tw = m.taggedTemplateExpression(m.memberExpression(m.identifier("tw")));
return {
visitor: {
JSXOpeningElement(path: NodePath<JSXOpeningElement>) {
const tw = findAttrbutes(path, "tw");
const css = findAttrbutes(path, "css");
const className = findAttrbutes(path, "className");
const elements: Array<Expression | SpreadElement | null> = [];
if (
tw &&
tw.type === "JSXAttribute" &&
tw.value?.type === "StringLiteral"
) {
elements.push(tw.value);
path.node.attributes = path.node.attributes.filter(n => n !== tw);
}
if (
className &&
className.type === "JSXAttribute" &&
className.value?.type === "StringLiteral"
) {
elements.push(className.value);
path.node.attributes = path.node.attributes.filter(
n => n !== className,
);
}
if (
css &&
css.type === "JSXAttribute" &&
css.value?.type === "JSXExpressionContainer" &&
css.value.expression.type === "ArrayExpression"
) {
elements.push(...css.value.expression.elements);
path.node.attributes = path.node.attributes.filter(n => n !== css);
}
function isStringLiteral(
e: Expression | SpreadElement | null,
): e is StringLiteral {
return e?.type === "StringLiteral";
}
const stringLiterals = elements.filter(e =>
isStringLiteral(e),
) as StringLiteral[];
const nonStringLiterals = elements.filter(e => !isStringLiteral(e));
const nonNullElements: Array<Expression | SpreadElement> = [];
if (stringLiterals.length > 0) {
const literal = stringLiterals.map(l => l.value).join(" ");
nonNullElements.push(t.stringLiteral(convertClasses(literal)));
}
nonNullElements.push(
...(nonStringLiterals.filter(Boolean) as Array<
Expression | SpreadElement
>),
);
const [first] = nonNullElements;
if (nonNullElements.length === 1 && first.type === "StringLiteral") {
path.node.attributes.push(
t.jsxAttribute(t.jsxIdentifier("className"), first),
);
} else if (
nonNullElements.length === 1 &&
first.type !== "SpreadElement"
) {
path.node.attributes.push(
t.jsxAttribute(
t.jsxIdentifier("className"),
t.jsxExpressionContainer(first),
),
);
} else if (nonNullElements.length > 0) {
path.node.attributes.push(
t.jsxAttribute(
t.jsxIdentifier("className"),
t.jsxExpressionContainer(
t.callExpression(t.identifier("classNames"), nonNullElements),
),
),
);
}
},
Identifier(path: NodePath<Identifier>) {
m.match(twStyle, {}, path.node, info => {
// Replaces "TwStyle" with "styled"
if (path.parent.type === "ImportSpecifier") return;
path.replaceWith(t.identifier("string") as any);
});
},
ImportDeclaration(path: NodePath<ImportDeclaration>) {
m.match(twimImport, {}, path.node, info => {
// Remove `import "twin.macro";`
path.remove();
});
},
TaggedTemplateExpression(path: NodePath<TaggedTemplateExpression>) {
m.match(tw, {}, path.node, info => {
// Replaces "tw.div`text-blue-500`;" with "html.div`text-blue-500`;"
if (path.node.tag.type === "MemberExpression") {
path.node.tag.object = t.identifier("html");
}
});
m.match(twimTemplate, {}, path.node, info => {
// Replaces "tw`text-blue-500`;" with just "text-blue-500"
if (path.node.quasi.expressions.length > 0) return;
const value = path.node.quasi.quasis[0].value.raw;
path.replaceWith(t.stringLiteral(convertClasses(value)) as any);
});
},
},
};
});
function findAttrbutes(path: NodePath<JSXOpeningElement>, name: string) {
return path.node.attributes.find(
attr => attr.type === "JSXAttribute" && attr.name.name === name,
);
}
function convertClasses(value) {
const arbitrary = value
.replace(/\s+/g, " ")
.replace(/(\S+)\[(.+?)\]/g, v => v.replace(/\s+/g, "_"))
.replaceAll("background[linear-gradient", "bg-[linear-gradient");
return arbitrary
.replace(/(xs|sm|md|lg|xl|2xl|3xl):\((.+?)\)/g, (_, a, b) => {
return b
.split(/\s+/)
.map(c => `${a}:${c}`)
.join(" ");
})
.trim();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment