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

Create styled-components to Tailwind converting flow #10

Merged
merged 8 commits into from
Jul 5, 2023
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
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -22,5 +29,8 @@
"prettier-plugin-tailwindcss": "^0.3.0",
"typescript": "^5.1.3",
"vitest": "^0.32.2"
},
"dependencies": {
"lodash": "^4.17.21"
}
}
72 changes: 72 additions & 0 deletions src/ASTtoCSS.ts
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") {
Copy link
Contributor

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

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
}
100 changes: 100 additions & 0 deletions src/CSStoTailwind.ts
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())
Copy link
Contributor

Choose a reason for hiding this comment

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

same as below remark ⬇️

let tailwindClasses = ""

cssRules.forEach((rule) => {
if (rule.includes("{")) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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 !== "") {
Copy link
Contributor

Choose a reason for hiding this comment

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

let's avoid if {} else statments, exit the function early instead of using else

tailwindClasses += `${selectorClasses} { ${declarationClasses} }\n\n`
} else {
tailwindClasses += `${declarationClasses}\n\n`
Copy link
Contributor

Choose a reason for hiding this comment

The 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)} `
Copy link
Contributor

Choose a reason for hiding this comment

The 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: {
Copy link
Contributor

Choose a reason for hiding this comment

The 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 {
Copy link
Contributor

Choose a reason for hiding this comment

The 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())
}
1 change: 0 additions & 1 deletion src/index.ts

This file was deleted.

73 changes: 73 additions & 0 deletions src/styledComponentsToAST.ts
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
}
Loading