diff --git a/__stubs__/asts.ts b/__stubs__/asts.ts new file mode 100644 index 0000000..380d2fe --- /dev/null +++ b/__stubs__/asts.ts @@ -0,0 +1,656 @@ +export const regularAST = { + type: "File", + start: 0, + end: 265, + loc: { start: { line: 1, column: 0, index: 0 }, end: { line: 13, column: 4, index: 265 } }, + errors: [], + program: { + type: "Program", + start: 0, + end: 265, + loc: { start: { line: 1, column: 0, index: 0 }, end: { line: 13, column: 4, index: 265 } }, + sourceType: "module", + interpreter: null, + body: [ + { + type: "ImportDeclaration", + start: 7, + end: 45, + loc: { start: { line: 2, column: 6, index: 7 }, end: { line: 2, column: 44, index: 45 } }, + importKind: "value", + specifiers: [ + { + type: "ImportDefaultSpecifier", + start: 14, + end: 20, + loc: { start: { line: 2, column: 13, index: 14 }, end: { line: 2, column: 19, index: 20 } }, + local: { + type: "Identifier", + start: 14, + end: 20, + loc: { + start: { line: 2, column: 13, index: 14 }, + end: { line: 2, column: 19, index: 20 }, + identifierName: "styled", + }, + name: "styled", + }, + }, + { + type: "ImportSpecifier", + local: { type: "Identifier", name: "styled" }, + imported: { type: "Identifier", name: "styled" }, + }, + ], + source: { + type: "StringLiteral", + start: 26, + end: 45, + loc: { start: { line: 2, column: 25, index: 26 }, end: { line: 2, column: 44, index: 45 } }, + extra: { rawValue: "styled-components", raw: "'styled-components'" }, + value: "styled-components", + }, + }, + { + type: "VariableDeclaration", + start: 53, + end: 260, + loc: { start: { line: 4, column: 6, index: 53 }, end: { line: 12, column: 7, index: 260 } }, + declarations: [ + { + type: "VariableDeclarator", + start: 59, + end: 260, + loc: { start: { line: 4, column: 12, index: 59 }, end: { line: 12, column: 7, index: 260 } }, + id: { + type: "Identifier", + start: 59, + end: 65, + loc: { + start: { line: 4, column: 12, index: 59 }, + end: { line: 4, column: 18, index: 65 }, + identifierName: "Button", + }, + name: "Button", + }, + init: { + type: "TaggedTemplateExpression", + start: 68, + end: 260, + loc: { start: { line: 4, column: 21, index: 68 }, end: { line: 12, column: 7, index: 260 } }, + tag: { + type: "MemberExpression", + start: 68, + end: 81, + loc: { start: { line: 4, column: 21, index: 68 }, end: { line: 4, column: 34, index: 81 } }, + object: { + type: "Identifier", + start: 68, + end: 74, + loc: { + start: { line: 4, column: 21, index: 68 }, + end: { line: 4, column: 27, index: 74 }, + identifierName: "styled", + }, + name: "styled", + }, + computed: false, + property: { + type: "Identifier", + start: 75, + end: 81, + loc: { + start: { line: 4, column: 28, index: 75 }, + end: { line: 4, column: 34, index: 81 }, + identifierName: "button", + }, + name: "button", + }, + }, + quasi: { + type: "TemplateLiteral", + start: 81, + end: 260, + loc: { start: { line: 4, column: 34, index: 81 }, end: { line: 12, column: 7, index: 260 } }, + expressions: [], + quasis: [ + { + type: "TemplateElement", + start: 82, + end: 259, + loc: { start: { line: 4, column: 35, index: 82 }, end: { line: 12, column: 6, index: 259 } }, + value: { + raw: "\n background: white;\n color: palevioletred;\n font-size: 1em;\n &:hover {\n background: palevioletred;\n color: white;\n }\n ", + cooked: + "\n background: white;\n color: palevioletred;\n font-size: 1em;\n &:hover {\n background: palevioletred;\n color: white;\n }\n ", + }, + tail: true, + }, + ], + }, + }, + }, + ], + kind: "const", + }, + ], + directives: [], + }, + comments: [], +} + +export const multipleDeclarationsAST = { + type: "File", + start: 0, + end: 427, + loc: { start: { line: 1, column: 0, index: 0 }, end: { line: 15, column: 4, index: 427 } }, + errors: [], + program: { + type: "Program", + start: 0, + end: 427, + loc: { start: { line: 1, column: 0, index: 0 }, end: { line: 15, column: 4, index: 427 } }, + sourceType: "module", + interpreter: null, + body: [ + { + type: "VariableDeclaration", + start: 7, + end: 333, + loc: { start: { line: 2, column: 6, index: 7 }, end: { line: 9, column: 7, index: 333 } }, + declarations: [ + { + type: "VariableDeclarator", + start: 13, + end: 333, + loc: { start: { line: 2, column: 12, index: 13 }, end: { line: 9, column: 7, index: 333 } }, + id: { + type: "Identifier", + start: 13, + end: 19, + loc: { + start: { line: 2, column: 12, index: 13 }, + end: { line: 2, column: 18, index: 19 }, + identifierName: "Button", + }, + name: "Button", + }, + init: { + type: "TaggedTemplateExpression", + start: 22, + end: 333, + loc: { start: { line: 2, column: 21, index: 22 }, end: { line: 9, column: 7, index: 333 } }, + tag: { + type: "MemberExpression", + start: 22, + end: 35, + loc: { start: { line: 2, column: 21, index: 22 }, end: { line: 2, column: 34, index: 35 } }, + object: { + type: "Identifier", + start: 22, + end: 28, + loc: { + start: { line: 2, column: 21, index: 22 }, + end: { line: 2, column: 27, index: 28 }, + identifierName: "styled", + }, + name: "styled", + }, + computed: false, + property: { + type: "Identifier", + start: 29, + end: 35, + loc: { + start: { line: 2, column: 28, index: 29 }, + end: { line: 2, column: 34, index: 35 }, + identifierName: "button", + }, + name: "button", + }, + }, + quasi: { + type: "TemplateLiteral", + start: 35, + end: 333, + loc: { start: { line: 2, column: 34, index: 35 }, end: { line: 9, column: 7, index: 333 } }, + expressions: [ + { + type: "ArrowFunctionExpression", + start: 59, + end: 100, + loc: { start: { line: 3, column: 22, index: 59 }, end: { line: 3, column: 63, index: 100 } }, + id: null, + generator: false, + async: false, + params: [ + { + type: "Identifier", + start: 59, + end: 64, + loc: { + start: { line: 3, column: 22, index: 59 }, + end: { line: 3, column: 27, index: 64 }, + identifierName: "props", + }, + name: "props", + }, + ], + body: { + type: "ConditionalExpression", + start: 68, + end: 100, + loc: { start: { line: 3, column: 31, index: 68 }, end: { line: 3, column: 63, index: 100 } }, + test: { + type: "MemberExpression", + start: 68, + end: 81, + loc: { start: { line: 3, column: 31, index: 68 }, end: { line: 3, column: 44, index: 81 } }, + object: { + type: "Identifier", + start: 68, + end: 73, + loc: { + start: { line: 3, column: 31, index: 68 }, + end: { line: 3, column: 36, index: 73 }, + identifierName: "props", + }, + name: "props", + }, + computed: false, + property: { + type: "Identifier", + start: 74, + end: 81, + loc: { + start: { line: 3, column: 37, index: 74 }, + end: { line: 3, column: 44, index: 81 }, + identifierName: "primary", + }, + name: "primary", + }, + }, + consequent: { + type: "StringLiteral", + start: 84, + end: 90, + loc: { start: { line: 3, column: 47, index: 84 }, end: { line: 3, column: 53, index: 90 } }, + extra: { rawValue: "blue", raw: "'blue'" }, + value: "blue", + }, + alternate: { + type: "StringLiteral", + start: 93, + end: 100, + loc: { start: { line: 3, column: 56, index: 93 }, end: { line: 3, column: 63, index: 100 } }, + extra: { rawValue: "white", raw: "'white'" }, + value: "white", + }, + }, + }, + { + type: "ArrowFunctionExpression", + start: 120, + end: 162, + loc: { start: { line: 4, column: 17, index: 120 }, end: { line: 4, column: 59, index: 162 } }, + id: null, + generator: false, + async: false, + params: [ + { + type: "Identifier", + start: 120, + end: 125, + loc: { + start: { line: 4, column: 17, index: 120 }, + end: { line: 4, column: 22, index: 125 }, + identifierName: "props", + }, + name: "props", + }, + ], + body: { + type: "ConditionalExpression", + start: 129, + end: 162, + loc: { start: { line: 4, column: 26, index: 129 }, end: { line: 4, column: 59, index: 162 } }, + test: { + type: "MemberExpression", + start: 129, + end: 142, + loc: { start: { line: 4, column: 26, index: 129 }, end: { line: 4, column: 39, index: 142 } }, + object: { + type: "Identifier", + start: 129, + end: 134, + loc: { + start: { line: 4, column: 26, index: 129 }, + end: { line: 4, column: 31, index: 134 }, + identifierName: "props", + }, + name: "props", + }, + computed: false, + property: { + type: "Identifier", + start: 135, + end: 142, + loc: { + start: { line: 4, column: 32, index: 135 }, + end: { line: 4, column: 39, index: 142 }, + identifierName: "primary", + }, + name: "primary", + }, + }, + consequent: { + type: "StringLiteral", + start: 145, + end: 152, + loc: { start: { line: 4, column: 42, index: 145 }, end: { line: 4, column: 49, index: 152 } }, + extra: { rawValue: "white", raw: "'white'" }, + value: "white", + }, + alternate: { + type: "StringLiteral", + start: 155, + end: 162, + loc: { start: { line: 4, column: 52, index: 155 }, end: { line: 4, column: 59, index: 162 } }, + extra: { rawValue: "black", raw: "'black'" }, + value: "black", + }, + }, + }, + { + type: "Identifier", + start: 186, + end: 194, + loc: { + start: { line: 5, column: 21, index: 186 }, + end: { line: 5, column: 29, index: 194 }, + identifierName: "fontSize", + }, + name: "fontSize", + }, + { + type: "ArrowFunctionExpression", + start: 254, + end: 295, + loc: { start: { line: 7, column: 28, index: 254 }, end: { line: 7, column: 69, index: 295 } }, + id: null, + generator: false, + async: false, + params: [ + { + type: "Identifier", + start: 254, + end: 259, + loc: { + start: { line: 7, column: 28, index: 254 }, + end: { line: 7, column: 33, index: 259 }, + identifierName: "props", + }, + name: "props", + }, + ], + body: { + type: "ConditionalExpression", + start: 263, + end: 295, + loc: { start: { line: 7, column: 37, index: 263 }, end: { line: 7, column: 69, index: 295 } }, + test: { + type: "MemberExpression", + start: 263, + end: 276, + loc: { start: { line: 7, column: 37, index: 263 }, end: { line: 7, column: 50, index: 276 } }, + object: { + type: "Identifier", + start: 263, + end: 268, + loc: { + start: { line: 7, column: 37, index: 263 }, + end: { line: 7, column: 42, index: 268 }, + identifierName: "props", + }, + name: "props", + }, + computed: false, + property: { + type: "Identifier", + start: 269, + end: 276, + loc: { + start: { line: 7, column: 43, index: 269 }, + end: { line: 7, column: 50, index: 276 }, + identifierName: "primary", + }, + name: "primary", + }, + }, + consequent: { + type: "StringLiteral", + start: 279, + end: 285, + loc: { start: { line: 7, column: 53, index: 279 }, end: { line: 7, column: 59, index: 285 } }, + extra: { rawValue: "blue", raw: "'blue'" }, + value: "blue", + }, + alternate: { + type: "StringLiteral", + start: 288, + end: 295, + loc: { start: { line: 7, column: 62, index: 288 }, end: { line: 7, column: 69, index: 295 } }, + extra: { rawValue: "black", raw: "'black'" }, + value: "black", + }, + }, + }, + ], + quasis: [ + { + type: "TemplateElement", + start: 36, + end: 57, + loc: { start: { line: 2, column: 35, index: 36 }, end: { line: 3, column: 20, index: 57 } }, + value: { raw: "\n background: ", cooked: "\n background: " }, + tail: false, + }, + { + type: "TemplateElement", + start: 101, + end: 118, + loc: { start: { line: 3, column: 64, index: 101 }, end: { line: 4, column: 15, index: 118 } }, + value: { raw: ";\n color: ", cooked: ";\n color: " }, + tail: false, + }, + { + type: "TemplateElement", + start: 163, + end: 184, + loc: { start: { line: 4, column: 60, index: 163 }, end: { line: 5, column: 19, index: 184 } }, + value: { raw: ";\n font-size: ", cooked: ";\n font-size: " }, + tail: false, + }, + { + type: "TemplateElement", + start: 195, + end: 252, + loc: { start: { line: 5, column: 30, index: 195 }, end: { line: 7, column: 26, index: 252 } }, + value: { + raw: ";\n padding: 0.25em 1em;\n border: 2px solid ", + cooked: ";\n padding: 0.25em 1em;\n border: 2px solid ", + }, + tail: false, + }, + { + type: "TemplateElement", + start: 296, + end: 332, + loc: { start: { line: 7, column: 70, index: 296 }, end: { line: 9, column: 6, index: 332 } }, + value: { + raw: ";\n border-radius: 3px;\n ", + cooked: ";\n border-radius: 3px;\n ", + }, + tail: true, + }, + ], + }, + }, + }, + ], + kind: "const", + }, + { + type: "VariableDeclaration", + start: 341, + end: 422, + loc: { start: { line: 11, column: 6, index: 341 }, end: { line: 14, column: 7, index: 422 } }, + declarations: [ + { + type: "VariableDeclarator", + start: 347, + end: 422, + loc: { start: { line: 11, column: 12, index: 347 }, end: { line: 14, column: 7, index: 422 } }, + id: { + type: "Identifier", + start: 347, + end: 353, + loc: { + start: { line: 11, column: 12, index: 347 }, + end: { line: 11, column: 18, index: 353 }, + identifierName: "Header", + }, + name: "Header", + }, + init: { + type: "TaggedTemplateExpression", + start: 356, + end: 422, + loc: { start: { line: 11, column: 21, index: 356 }, end: { line: 14, column: 7, index: 422 } }, + tag: { + type: "MemberExpression", + start: 356, + end: 365, + loc: { start: { line: 11, column: 21, index: 356 }, end: { line: 11, column: 30, index: 365 } }, + object: { + type: "Identifier", + start: 356, + end: 362, + loc: { + start: { line: 11, column: 21, index: 356 }, + end: { line: 11, column: 27, index: 362 }, + identifierName: "styled", + }, + name: "styled", + }, + computed: false, + property: { + type: "Identifier", + start: 363, + end: 365, + loc: { + start: { line: 11, column: 28, index: 363 }, + end: { line: 11, column: 30, index: 365 }, + identifierName: "h1", + }, + name: "h1", + }, + }, + quasi: { + type: "TemplateLiteral", + start: 365, + end: 422, + loc: { start: { line: 11, column: 30, index: 365 }, end: { line: 14, column: 7, index: 422 } }, + expressions: [], + quasis: [ + { + type: "TemplateElement", + start: 366, + end: 421, + loc: { start: { line: 11, column: 31, index: 366 }, end: { line: 14, column: 6, index: 421 } }, + value: { + raw: "\n font-size: 1rem;\n padding: 4rem;\n ", + cooked: "\n font-size: 1rem;\n padding: 4rem;\n ", + }, + tail: true, + }, + ], + }, + }, + }, + ], + kind: "const", + }, + ], + directives: [], + }, + comments: [], +} + +export const nestedTemplateAST = { + type: "TaggedTemplateExpression", + tag: { + type: "MemberExpression", + object: { + type: "Identifier", + name: "styled", + }, + property: { + type: "Identifier", + name: "section", + }, + }, + quasi: { + type: "TemplateLiteral", + quasis: [ + { + type: "TemplateElement", + value: { + raw: "\n display: flex;\n justify-content: center;\n align-items: center;\n height: 100vh;\n background: linear-gradient(\n 145deg,\n rgba(253, 38, 71, 1) 0%,\n rgba(252, 128, 45, 1) 75%,\n rgba(250, 167, 43, 1) 100%\n );\n\n ", + cooked: + "\n display: flex;\n justify-content: center;\n align-items: center;\n height: 100vh;\n background: linear-gradient(\n 145deg,\n rgba(253, 38, 71, 1) 0%,\n rgba(252, 128, 45, 1) 75%,\n rgba(250, 167, 43, 1) 100%\n );\n\n ", + }, + tail: false, + }, + { + type: "TemplateElement", + value: { + raw: "\n\n ", + cooked: "\n\n ", + }, + tail: false, + }, + { + type: "TemplateElement", + value: { + raw: "\n", + cooked: "\n", + }, + tail: true, + }, + ], + expressions: [ + { + type: "TaggedTemplateExpression", + tag: { + type: "Identifier", + name: "css", + }, + quasi: { + type: "TemplateLiteral", + quasis: [ + { + type: "TemplateElement", + value: { + raw: "\n margin-bottom: 2em;\n ", + cooked: "\n margin-bottom: 2em;\n ", + }, + tail: true, + }, + ], + expressions: [], + }, + }, + ], + }, +} diff --git a/src/ASTtoCSS.ts b/src/ASTtoCSS.ts index 27586c5..8347fb3 100644 --- a/src/ASTtoCSS.ts +++ b/src/ASTtoCSS.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-prototype-builtins */ import { Node } from "@babel/core" import * as t from "@babel/types" @@ -20,15 +19,17 @@ export function generateCSSFromAST(ast: Node): string { 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() + const cssRule = node.quasi.quasis + .map((element) => element.value.raw) + .join("") + .trim() if (cssRule.length > 0) { - let randomSelector = generateRandomSelector() - while (generatedSelectors.has(randomSelector)) { + let randomSelector + + do { randomSelector = generateRandomSelector() - } + } while (generatedSelectors.has(randomSelector)) const selector = `${selectorPrefix}.${randomSelector}` generatedSelectors.add(randomSelector) @@ -36,34 +37,24 @@ export function generateCSSFromAST(ast: Node): string { } } - 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) - } - } - } + Object.values(node).forEach((childNode) => { + if (!childNode || typeof childNode !== "object") return + + if (Array.isArray(childNode)) { + childNode.forEach((child) => traverseAST(child, selectorPrefix)) + } else if (t.isJSXElement(childNode)) { + const { openingElement, closingElement } = childNode + + ;[openingElement, closingElement].forEach((element) => { + if (!element || !t.isJSXIdentifier(element.name)) return + + const childSelector = `${selectorPrefix} ${element.name.name}` + traverseAST(element, childSelector) + }) + } else { + traverseAST(childNode, selectorPrefix) } - } + }) } traverseAST(ast) diff --git a/src/CSStoTailwind.ts b/src/CSStoTailwind.ts index 79e8a02..c1657e4 100644 --- a/src/CSStoTailwind.ts +++ b/src/CSStoTailwind.ts @@ -1,45 +1,36 @@ +import _ from "lodash" + export function convertToTailwindCSS(cssCode: string): string { - const cssRules = cssCode.split("}").map((rule) => rule.trim()) - let tailwindClasses = "" + const CSS_BLOCKS = cssCode.split("}").map((block) => block.trim()) + + const tailwindClasses = CSS_BLOCKS.filter((block) => block.includes("{")).map((block) => { + const [selectors, declarations] = block.split("{").map((part) => part.trim()) + const selectorClasses = extractSelectorClasses(selectors) + const declarationClasses = extractDeclarationClasses(declarations) - cssRules.forEach((rule) => { - if (rule.includes("{")) { - const [selectors, declarations] = rule.split("{").map((part) => part.trim()) - const selectorClasses = getSelectorClasses(selectors) - const declarationClasses = getDeclarationClasses(declarations) - if (selectorClasses !== "") { - tailwindClasses += `${selectorClasses} { ${declarationClasses} }\n\n` - } else { - tailwindClasses += `${declarationClasses}\n\n` - } - } + return selectorClasses ? `${selectorClasses} { ${declarationClasses.join(" ")} }` : declarationClasses.join(" ") }) - return tailwindClasses.trim() + return tailwindClasses.join(" ") } -function getSelectorClasses(selectors: string): string { +function extractSelectorClasses(selectors: string): string { const classes = selectors.split(",").map((item) => item.trim()) + return classes.join(", ") } -function getDeclarationClasses(declarations: string): string { +function extractDeclarationClasses(declarations: string): string[] { const properties = declarations.split(";").map((declaration) => declaration.trim()) - let classes = "" - properties.forEach((property) => { - if (property.includes(":")) { + return properties + .filter((property) => property.includes(":")) + .map((property) => { const [propertyKey, propertyValue] = property.split(":").map((part) => part.trim()) const tailwindClass = getTailwindClass(propertyKey, propertyValue) - if (tailwindClass) { - classes += `${tailwindClass} ` - } else { - classes += `${getPropertyAlias(propertyKey)}-${wrapWithArbitraryValue(propertyValue)} ` - } - } - }) - return classes.trim() + return tailwindClass ? tailwindClass : `${getPropertyAlias(propertyKey)}-${wrapWithArbitraryValue(propertyValue)}` + }) } function getTailwindClass(property: string, value: string): string | null { @@ -69,12 +60,9 @@ function getTailwindClass(property: string, value: string): string | null { }, } - const propertyMapping = propertyMappings[property] || propertyMappings[toCamelCase(property)] - if (propertyMapping && propertyMapping[value]) { - return propertyMapping[value] - } + const propertyMapping = propertyMappings[property] || propertyMappings[_.camelCase(property)] - return null + return propertyMapping && propertyMapping[value] ? propertyMapping[value] : null } function getPropertyAlias(property: string): string { @@ -83,9 +71,6 @@ function getPropertyAlias(property: string): string { color: "text", fontSize: "text", fontWeight: "font", - // Add more property aliases here - // Example: - // textDecoration: "underline", } return propertyAliases[property] || property @@ -94,7 +79,3 @@ function getPropertyAlias(property: string): string { function wrapWithArbitraryValue(value: string): string { return `[${value}]` } - -function toCamelCase(str: string): string { - return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()) -} diff --git a/src/styledComponentsToAST.ts b/src/styledComponentsToAST.ts index a657bcb..3f0d201 100644 --- a/src/styledComponentsToAST.ts +++ b/src/styledComponentsToAST.ts @@ -9,7 +9,7 @@ export function transformCodeToAST(input: string) { throw new Error("Provided input is empty") } - let ast + let ast: t.Node try { ast = parse(input, { sourceType: "module", @@ -45,7 +45,7 @@ export function transformCodeToAST(input: string) { const { declarations } = path.node declarations.forEach((declaration) => { - const { id, init } = declaration + const { init } = declaration if (t.isCallExpression(init) && t.isIdentifier(init.callee, { name: styledIdentifier.name })) { const styledCallExpression = t.callExpression( diff --git a/test/ASTtoCSS.test.js b/test/ASTtoCSS.test.js index c2c5f75..f6bd7f7 100644 --- a/test/ASTtoCSS.test.js +++ b/test/ASTtoCSS.test.js @@ -1,177 +1,50 @@ import { describe, it, expect } from "vitest" import { generateCSSFromAST } from "../src/ASTtoCSS" import { stripWhitespace } from "./test-utils" - -const input = { - type: "File", - start: 0, - end: 265, - loc: { start: { line: 1, column: 0, index: 0 }, end: { line: 13, column: 4, index: 265 } }, - errors: [], - program: { - type: "Program", - start: 0, - end: 265, - loc: { start: { line: 1, column: 0, index: 0 }, end: { line: 13, column: 4, index: 265 } }, - sourceType: "module", - interpreter: null, - body: [ - { - type: "ImportDeclaration", - start: 7, - end: 45, - loc: { start: { line: 2, column: 6, index: 7 }, end: { line: 2, column: 44, index: 45 } }, - importKind: "value", - specifiers: [ - { - type: "ImportDefaultSpecifier", - start: 14, - end: 20, - loc: { start: { line: 2, column: 13, index: 14 }, end: { line: 2, column: 19, index: 20 } }, - local: { - type: "Identifier", - start: 14, - end: 20, - loc: { - start: { line: 2, column: 13, index: 14 }, - end: { line: 2, column: 19, index: 20 }, - identifierName: "styled", - }, - name: "styled", - }, - }, - { - type: "ImportSpecifier", - local: { type: "Identifier", name: "styled" }, - imported: { type: "Identifier", name: "styled" }, - }, - ], - source: { - type: "StringLiteral", - start: 26, - end: 45, - loc: { start: { line: 2, column: 25, index: 26 }, end: { line: 2, column: 44, index: 45 } }, - extra: { rawValue: "styled-components", raw: "'styled-components'" }, - value: "styled-components", - }, - }, - { - type: "VariableDeclaration", - start: 53, - end: 260, - loc: { start: { line: 4, column: 6, index: 53 }, end: { line: 12, column: 7, index: 260 } }, - declarations: [ - { - type: "VariableDeclarator", - start: 59, - end: 260, - loc: { start: { line: 4, column: 12, index: 59 }, end: { line: 12, column: 7, index: 260 } }, - id: { - type: "Identifier", - start: 59, - end: 65, - loc: { - start: { line: 4, column: 12, index: 59 }, - end: { line: 4, column: 18, index: 65 }, - identifierName: "Button", - }, - name: "Button", - }, - init: { - type: "TaggedTemplateExpression", - start: 68, - end: 260, - loc: { start: { line: 4, column: 21, index: 68 }, end: { line: 12, column: 7, index: 260 } }, - tag: { - type: "MemberExpression", - start: 68, - end: 81, - loc: { start: { line: 4, column: 21, index: 68 }, end: { line: 4, column: 34, index: 81 } }, - object: { - type: "Identifier", - start: 68, - end: 74, - loc: { - start: { line: 4, column: 21, index: 68 }, - end: { line: 4, column: 27, index: 74 }, - identifierName: "styled", - }, - name: "styled", - }, - computed: false, - property: { - type: "Identifier", - start: 75, - end: 81, - loc: { - start: { line: 4, column: 28, index: 75 }, - end: { line: 4, column: 34, index: 81 }, - identifierName: "button", - }, - name: "button", - }, - }, - quasi: { - type: "TemplateLiteral", - start: 81, - end: 260, - loc: { start: { line: 4, column: 34, index: 81 }, end: { line: 12, column: 7, index: 260 } }, - expressions: [], - quasis: [ - { - type: "TemplateElement", - start: 82, - end: 259, - loc: { start: { line: 4, column: 35, index: 82 }, end: { line: 12, column: 6, index: 259 } }, - value: { - raw: "\n background: white;\n color: palevioletred;\n font-size: 1em;\n &:hover {\n background: palevioletred;\n color: white;\n }\n ", - cooked: - "\n background: white;\n color: palevioletred;\n font-size: 1em;\n &:hover {\n background: palevioletred;\n color: white;\n }\n ", - }, - tail: true, - }, - ], - }, - }, - }, - ], - kind: "const", - }, - ], - directives: [], - }, - comments: [], -} +import { regularAST, multipleDeclarationsAST, nestedTemplateAST } from "../__stubs__/asts" describe("#generateCSSFromAST", () => { - it("Should generate valid CSS from correct AST input", () => { - const result = stripWhitespace(generateCSSFromAST(input)) + it("should generate valid CSS from correct AST input", () => { + const result = stripWhitespace(generateCSSFromAST(regularAST)) expect(result).toEqual( expect.stringContaining( - "{background:white;color:palevioletred;font-size:1em;&:hover{background:palevioletred;color:white;}}" + "background:white;color:palevioletred;font-size:1em;&:hover{background:palevioletred;color:white;" ) ) }) - it("Should generate valid CSS from empty AST input", () => { - const input = `` + it("should generate valid CSS from empty AST input", () => { + const input = "" const result = stripWhitespace(generateCSSFromAST(input)) expect(result).toEqual("") // Expect an empty string since there are no CSS rules }) - it("Should generate valid CSS from AST input with multiple tagged template expressions", () => { - const result = stripWhitespace(generateCSSFromAST(input)) + it("should generate valid CSS from AST input with multiple tagged template expressions", () => { + const result = stripWhitespace(generateCSSFromAST(multipleDeclarationsAST)) + + expect(result).toEqual( + expect.stringContaining( + stripWhitespace("background:;color:;font-size:;padding:0.25em1em;border:2pxsolid;border-radius:3px;") + ) + ) - expect(result).toEqual(expect.stringContaining("background:white;color:palevioletred;font-size:1em;")) - expect(result).toEqual(expect.stringContaining("&:hover{background:palevioletred;color:white;}")) + expect(result).toEqual(expect.stringContaining(stripWhitespace("font-size:1rem;padding:4rem;"))) }) - it("Should generate valid CSS from AST input with nested tagged template expressions", () => { - const result = stripWhitespace(generateCSSFromAST(input)) + it("should generate valid CSS from AST input with nested tagged template expressions", () => { + const result = stripWhitespace(generateCSSFromAST(nestedTemplateAST)) + + expect(result).toEqual( + expect.stringContaining( + stripWhitespace( + "display: flex; justify-content:center;align-items:center;height:100vh;background:linear-gradient(145deg,rgba(253, 38, 71, 1) 0%,rgba(252, 128, 45, 1)75%,rgba(250, 167, 43, 1)100%);" + ) + ) + ) - expect(result).toEqual(expect.stringContaining("&:hover{background:palevioletred;color:white;}")) + expect(result).toEqual(expect.stringContaining(stripWhitespace("margin-bottom: 2em;"))) }) }) diff --git a/test/CSStoTailwind.test.js b/test/CSStoTailwind.test.js index 68258e6..674d02a 100644 --- a/test/CSStoTailwind.test.js +++ b/test/CSStoTailwind.test.js @@ -3,7 +3,7 @@ import { convertToTailwindCSS } from "../src/CSStoTailwind" import { stripWhitespace } from "./test-utils" describe("#convertToTailwind", () => { - it("Should convert CSS input to Tailwind", () => { + it("should convert CSS input to Tailwind", () => { const input = ` .button { background-color: white; @@ -12,15 +12,31 @@ describe("#convertToTailwind", () => { } ` - const result = convertToTailwindCSS(input) + const result = stripWhitespace(convertToTailwindCSS(input)) - expect(stripWhitespace(result)).toEqual(expect.stringContaining("{bg-whitefont-size-[16px]padding-[7px]}")) + expect(result).toEqual(expect.stringContaining("{bg-whitefont-size-[16px]padding-[7px]}")) }) - it("Should convert CSS code generated from AST into valid CSS", () => { - const input = `.mzoqw { background-color: white; color: red; font-size: 1rem; margin: 4rem; }` + it("should convert CSS code generated from AST to valid TailwindCSS", () => { + const input = stripWhitespace(".mzoqw { background-color: white; color: red; font-size: 1rem; margin: 4rem; }") const result = convertToTailwindCSS(input) - expect(stripWhitespace(result)).toEqual(expect.stringContaining("{bg-whitetext-red-500text-basem-16}")) + expect(result).toEqual(expect.stringContaining("{ bg-white text-red-500 text-base m-16 }")) + }) + + it("should convert invalid CSS properties to TailwindCSS", () => { + // TODO: Not yet. 🦇 + }) + + it("should convert prefixed CSS properties (i.e. --webkit) to TailwindCSS", () => { + // TODO: Not yet. 🦇 + }) + + it("should convert shorthand CSS properties (i.e. padding: 1px 0 3px 5px) to TailwindCSS", () => { + // TODO: Not yet. 🦇 + }) + + it("should convert CSS properties with a decimal to TailwindCSS", () => { + // TODO: Not yet. 🦇 }) }) diff --git a/test/index.test.js b/test/index.test.js index 037b530..378e2ef 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -5,7 +5,7 @@ import { generateCSSFromAST } from "../src/ASTtoCSS" import { convertToTailwindCSS } from "../src/CSStoTailwind" describe("styled-components to TailwindCSS converter flow", () => { - it("Should correctly transform valid JS input including styled-components into TailwindCSS utility classes", () => { + it("should correctly transform valid JS input including styled-components into TailwindCSS utility classes", () => { const input = ` import styled from 'styled-components' const Button = styled.button\` diff --git a/test/styledComponentsToAST.test.js b/test/styledComponentsToAST.test.js index aa7af13..659ffb2 100644 --- a/test/styledComponentsToAST.test.js +++ b/test/styledComponentsToAST.test.js @@ -3,7 +3,7 @@ import { transformCodeToAST } from "../src/styledComponentsToAST" import { findPropertyByValue } from "./test-utils" describe("#transformCodeToAST", () => { - it("Should contain a valid TemplateElement type", () => { + it("should contain a valid TemplateElement type", () => { const input = ` import styled from 'styled-components' const Button = styled.button\` @@ -19,7 +19,7 @@ describe("#transformCodeToAST", () => { expect(findPropertyByValue(transformCodeToAST(input), "type", "TemplateElement")).toBeTruthy() }) - it("Should contain properties that are equal to the input code", () => { + it("should contain properties that are equal to the input code", () => { const input = ` const Button = \` background: white; @@ -40,13 +40,14 @@ describe("#transformCodeToAST", () => { expect(result).toBeTruthy() }) - it("Should throw an error if the input is empty", () => { + it("should throw an error if the input is empty", () => { const input = "" expect(() => transformCodeToAST(input)).toThrowError(/^Provided input is empty$/) + expect(() => transformCodeToAST(input)).not.toThrowError(/^Any other error/) }) - it("Should throw an error if the input is not valid JavaScript code", () => { + it("should throw an error if the input is not valid JavaScript code", () => { const input = ` import styled from 'styled @@ -63,7 +64,7 @@ describe("#transformCodeToAST", () => { expect(() => transformCodeToAST(input)).toThrowError(/^Input is not valid JavaScript code/) }) - it("Should transform styled-component CSS interpolation into AST", () => { + it("should transform styled-component CSS interpolation into AST", () => { const input = ` const Button = styled.button\` background: \${props => props.primary ? 'blue' : 'white'};