-
Notifications
You must be signed in to change notification settings - Fork 8
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
Create styled-components to Tailwind converting flow #10
Changes from 7 commits
f491ab7
95172bb
e6fb0ca
12a5aeb
f6caad1
cbafc79
fcad390
def31cb
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 |
---|---|---|
|
@@ -5,14 +5,21 @@ | |
"type": "module", | ||
"scripts": { | ||
"build": "tsc", | ||
"convertToAST": "node dist/styledComponentsToAST.js", | ||
"test": "vitest", | ||
"coverage": "vitest run --coverage" | ||
}, | ||
"repository": "[email protected]:Blazity/styled2tailwind.git", | ||
"author": "Blazity", | ||
"license": "MIT", | ||
"devDependencies": { | ||
"@babel/core": "^7.22.5", | ||
"@babel/traverse": "^7.22.5", | ||
"@babel/types": "^7.22.5", | ||
"@total-typescript/ts-reset": "^0.4.2", | ||
"@types/babel__core": "^7.20.1", | ||
"@types/babel__traverse": "^7.20.1", | ||
"@types/lodash": "^4.14.195", | ||
"@types/node": "^20.3.1", | ||
"@typescript-eslint/eslint-plugin": "^5.60.0", | ||
"@typescript-eslint/parser": "^5.60.0", | ||
|
@@ -22,5 +29,8 @@ | |
"prettier-plugin-tailwindcss": "^0.3.0", | ||
"typescript": "^5.1.3", | ||
"vitest": "^0.32.2" | ||
}, | ||
"dependencies": { | ||
"lodash": "^4.17.21" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
/* eslint-disable no-prototype-builtins */ | ||
import { Node } from "@babel/core" | ||
import * as t from "@babel/types" | ||
|
||
export function generateCSSFromAST(ast: Node): string { | ||
let cssCode = "" | ||
const generatedSelectors: Set<string> = new Set() | ||
|
||
function generateRandomSelector(): string { | ||
const characters = "abcdefghijklmnopqrstuvwxyz" | ||
let randomSelector = "" | ||
|
||
for (let i = 0; i < 5; i++) { | ||
const randomIndex = Math.floor(Math.random() * characters.length) | ||
randomSelector += characters.charAt(randomIndex) | ||
} | ||
|
||
return randomSelector | ||
} | ||
|
||
function traverseAST(node: Node, selectorPrefix = "") { | ||
if (t.isTaggedTemplateExpression(node)) { | ||
const { quasi } = node | ||
const templateElements = quasi.quasis.map((element) => element.value.raw) | ||
const cssRule = templateElements.join("").trim() | ||
|
||
if (cssRule.length > 0) { | ||
let randomSelector = generateRandomSelector() | ||
while (generatedSelectors.has(randomSelector)) { | ||
randomSelector = generateRandomSelector() | ||
} | ||
|
||
const selector = `${selectorPrefix}.${randomSelector}` | ||
generatedSelectors.add(randomSelector) | ||
cssCode += `${selector} { ${cssRule} }\n` | ||
} | ||
} | ||
|
||
if (node && typeof node === "object") { | ||
for (const key in node) { | ||
if (node.hasOwnProperty(key)) { | ||
const childNode = node[key] | ||
if (Array.isArray(childNode)) { | ||
childNode.forEach((child) => traverseAST(child, selectorPrefix)) | ||
} else if (childNode && typeof childNode === "object") { | ||
if (t.isJSXElement(childNode)) { | ||
const { openingElement, closingElement } = childNode | ||
const { name } = openingElement | ||
if (t.isJSXIdentifier(name)) { | ||
const childSelector = `${selectorPrefix} ${name.name}` | ||
traverseAST(childNode, childSelector) | ||
} | ||
if (closingElement) { | ||
const { name: closingName } = closingElement | ||
if (t.isJSXIdentifier(closingName)) { | ||
const childSelector = `${selectorPrefix} ${closingName.name}` | ||
traverseAST(closingElement, childSelector) | ||
} | ||
} | ||
} else { | ||
traverseAST(childNode, selectorPrefix) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
traverseAST(ast) | ||
|
||
return cssCode | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
export function convertToTailwindCSS(cssCode: string): string { | ||
const cssRules = cssCode.split("}").map((rule) => rule.trim()) | ||
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. same as below remark ⬇️ |
||
let tailwindClasses = "" | ||
|
||
cssRules.forEach((rule) => { | ||
if (rule.includes("{")) { | ||
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. not sure what "{" means in that context, could we extract it to a well named const variable? CASED_LIKE_THIS |
||
const [selectors, declarations] = rule.split("{").map((part) => part.trim()) | ||
const selectorClasses = getSelectorClasses(selectors) | ||
const declarationClasses = getDeclarationClasses(declarations) | ||
if (selectorClasses !== "") { | ||
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. let's avoid |
||
tailwindClasses += `${selectorClasses} { ${declarationClasses} }\n\n` | ||
} else { | ||
tailwindClasses += `${declarationClasses}\n\n` | ||
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's the reasoning behind adding \n\n? could we document it here? If it is possible to document it using only code then great, if it's not then use comments |
||
} | ||
} | ||
}) | ||
|
||
return tailwindClasses.trim() | ||
} | ||
|
||
function getSelectorClasses(selectors: string): string { | ||
const classes = selectors.split(",").map((item) => item.trim()) | ||
return classes.join(", ") | ||
} | ||
|
||
function getDeclarationClasses(declarations: string): string { | ||
const properties = declarations.split(";").map((declaration) => declaration.trim()) | ||
let classes = "" | ||
|
||
properties.forEach((property) => { | ||
if (property.includes(":")) { | ||
const [propertyKey, propertyValue] = property.split(":").map((part) => part.trim()) | ||
const tailwindClass = getTailwindClass(propertyKey, propertyValue) | ||
if (tailwindClass) { | ||
classes += `${tailwindClass} ` | ||
} else { | ||
classes += `${getPropertyAlias(propertyKey)}-${wrapWithArbitraryValue(propertyValue)} ` | ||
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. we gotta make it somehow cleaner, I would suggest removing string interpolation with ` sign and use array and join instead (everywhere). Template literals might look like a good idea but in the end they create very ugly and hard to maintain code |
||
} | ||
} | ||
}) | ||
|
||
return classes.trim() | ||
} | ||
|
||
function getTailwindClass(property: string, value: string): string | null { | ||
const propertyMappings: Record<string, Record<string, string>> = { | ||
backgroundColor: { | ||
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. looks awesome, good job 🖖🏻 |
||
transparent: "bg-transparent", | ||
current: "bg-current", | ||
black: "bg-black", | ||
white: "bg-white", | ||
default: `bg-[${value}]`, | ||
}, | ||
margin: { | ||
"4rem": "m-16", | ||
default: `m-[${value}]`, | ||
}, | ||
fontSize: { | ||
"1rem": "text-base", | ||
default: `text-[${value}]`, | ||
}, | ||
color: { | ||
red: "text-red-500", | ||
default: `text-[${value}]`, | ||
}, | ||
padding: { | ||
"0.25rem": "p-1", | ||
default: `p-[${value}]`, | ||
}, | ||
} | ||
|
||
const propertyMapping = propertyMappings[property] || propertyMappings[toCamelCase(property)] | ||
if (propertyMapping && propertyMapping[value]) { | ||
return propertyMapping[value] | ||
} | ||
|
||
return null | ||
} | ||
|
||
function getPropertyAlias(property: string): string { | ||
const propertyAliases: Record<string, string> = { | ||
backgroundColor: "bg", | ||
color: "text", | ||
fontSize: "text", | ||
fontWeight: "font", | ||
// Add more property aliases here | ||
// Example: | ||
// textDecoration: "underline", | ||
} | ||
|
||
return propertyAliases[property] || property | ||
} | ||
|
||
function wrapWithArbitraryValue(value: string): string { | ||
return `[${value}]` | ||
} | ||
|
||
function toCamelCase(str: string): string { | ||
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. let's remove this and use lodash, it's always a good idea to use well-known and battle-tested library instead of writing our own implementations |
||
return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()) | ||
} |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
/* eslint-disable no-prototype-builtins */ | ||
import { parse, ParserOptions } from "@babel/parser" | ||
import { traverse } from "@babel/core" | ||
import { NodePath } from "@babel/traverse" | ||
import * as t from "@babel/types" | ||
|
||
export function transformCodeToAST(input: string) { | ||
if (!input) { | ||
throw new Error("Provided input is empty") | ||
} | ||
|
||
let ast | ||
try { | ||
ast = parse(input, { | ||
sourceType: "module", | ||
plugins: ["jsx", "typescript"], | ||
} as ParserOptions) | ||
} catch (error) { | ||
throw new Error("Input is not valid JavaScript code") | ||
} | ||
|
||
const styledIdentifier = t.identifier("styled") | ||
const styledProperty = t.identifier("styled") | ||
|
||
traverse(ast, { | ||
TaggedTemplateExpression(path: NodePath<t.TaggedTemplateExpression>) { | ||
const { tag, quasi } = path.node | ||
|
||
if ( | ||
t.isMemberExpression(tag) && | ||
t.isIdentifier(tag.object, { name: styledIdentifier.name }) && | ||
t.isIdentifier(tag.property, { name: styledProperty.name }) | ||
) { | ||
const styledCallExpression = t.callExpression(t.memberExpression(styledIdentifier, styledProperty), [ | ||
tag.object, | ||
tag.property, | ||
quasi, | ||
]) | ||
|
||
path.replaceWith(styledCallExpression) | ||
} | ||
}, | ||
|
||
VariableDeclaration(path: NodePath<t.VariableDeclaration>) { | ||
const { declarations } = path.node | ||
|
||
declarations.forEach((declaration) => { | ||
const { id, init } = declaration | ||
|
||
if (t.isCallExpression(init) && t.isIdentifier(init.callee, { name: styledIdentifier.name })) { | ||
const styledCallExpression = t.callExpression( | ||
t.memberExpression(styledIdentifier, styledProperty), | ||
init.arguments | ||
) | ||
|
||
declaration.init = styledCallExpression | ||
} | ||
}) | ||
}, | ||
|
||
ImportDeclaration(path: NodePath<t.ImportDeclaration>) { | ||
const { source, specifiers } = path.node | ||
|
||
if (source.value === "styled-components") { | ||
const importSpecifier = t.importSpecifier(styledIdentifier, styledProperty) | ||
|
||
specifiers.push(importSpecifier) | ||
} | ||
}, | ||
}) | ||
|
||
return ast | ||
} |
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.
we should hide the complexity from the exposed function, it should be included in other file that contains such implementation details. My reasoning behind that is that, if it's not hidden, it increases the visual complexity of the code and makes a developer afraid of the code