Skip to content

Commit

Permalink
Fix issues with variable interpolation in classnames
Browse files Browse the repository at this point in the history
  • Loading branch information
Dimitri BARBOT committed Jan 20, 2023
1 parent a5a9b25 commit b72f621
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 81 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "tailwind-styled-components-extractor",
"displayName": "Tailwind Styled-Components Extractor",
"version": "0.3.0",
"version": "0.3.1",
"description": "Generate tailwind styled-components from JSX tags. A faster tailwind styled-component workflow.",
"license": "MIT",
"publisher": "dimitribarbot",
Expand Down
20 changes: 11 additions & 9 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const extractCurrentToSeparateFile = async (
);
if (!styleFile) {
vscode.window.showWarningMessage(
"[SCE] This file does not match the pattern in your configuration."
"[TSCE] This file does not match the pattern in your configuration."
);
return;
}
Expand Down Expand Up @@ -145,7 +145,7 @@ const extractUnboundToClipboard = async (
await vscode.env.clipboard.writeText(clipboardText);

vscode.window.showInformationMessage(
`[SCE] Copied to clipboard! (Found: ${components.length}) `
`[TSCE] Copied to clipboard! (Found: ${components.length}) `
);
};

Expand All @@ -165,7 +165,7 @@ const extractUnboundToSeparateFile = async (
);
if (!styleFile) {
vscode.window.showWarningMessage(
"[SCE] This file does not match the pattern in your configuration."
"[TSCE] This file does not match the pattern in your configuration."
);
return;
}
Expand Down Expand Up @@ -214,7 +214,7 @@ const extract = async (type: ExtractType): Promise<void> => {
!/\.(js|ts)x?$/.test(editor.document.fileName)
) {
vscode.window.showWarningMessage(
"[SCE] Only `.js`, `.ts`, `.jsx` and `.tsx` are supported"
"[TSCE] Only `.js`, `.ts`, `.jsx` and `.tsx` are supported"
);
return;
}
Expand All @@ -238,7 +238,7 @@ const extract = async (type: ExtractType): Promise<void> => {
const component = getUnderlyingComponent(text, offset);
if (!component) {
vscode.window.showWarningMessage(
"[SCE] Nothing to extract: There is no underlying component"
"[TSCE] Nothing to extract: There is no underlying component"
);
return;
}
Expand Down Expand Up @@ -269,7 +269,7 @@ const extract = async (type: ExtractType): Promise<void> => {
const components = collectUnboundComponents(text);
if (!components.length) {
vscode.window.showWarningMessage(
"[SCE] Nothing to extract: There are no unbound components"
"[TSCE] Nothing to extract: There are no unbound components"
);
return;
}
Expand Down Expand Up @@ -303,11 +303,13 @@ const extract = async (type: ExtractType): Promise<void> => {
} catch (e) {
if (e instanceof Error && Object.getPrototypeOf(e).name === "SyntaxError") {
vscode.window.showErrorMessage(
`[SCE] Failed to extract due to syntax error: ${e.message}`
`[TSCE] Failed to extract due to syntax error: ${e.message}`
);
} else {
console.error("[SCE]", e);
vscode.window.showErrorMessage("[SCE] Unexpected error while extracting");
console.error("[TSCE]", e);
vscode.window.showErrorMessage(
"[TSCE] Unexpected error while extracting"
);
}
}
};
Expand Down
22 changes: 11 additions & 11 deletions src/lib/extractor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,12 @@ describe("collectUnboundComponents", () => {
const TestComponent: React.FC = ({ a }) => {
const b = a?.b
const c = b ?? c
const c = b ?? a?.d
return (
<Abc className="flex flex-col">
<Def>
<Efg className={c ? "justify-center" : "justify-start"} />
<Ghi className={\`flex flex-col \${c && "flex"} \${(a && b) || c ? "justify-center" : "justify-start"}\`} />
<Ghi className={\`flex flex-col \${c(a?.e) && "flex"} \${(a && b) || c ? "justify-center" : "justify-start"}\`} />
<Efg className="justify-center" />
<Ghi />
<section />
Expand All @@ -89,23 +89,23 @@ describe("collectUnboundComponents", () => {
{
name: "Abc",
className: "flex flex-col",
classNameOffsets: { start: 138, end: 163 }
classNameOffsets: { start: 141, end: 166 }
},
{
name: "Efg",
className: '${({ c }) => c ? "justify-center" : "justify-start"}',
classNameOffsets: { start: 194, end: 244 }
classNameOffsets: { start: 197, end: 247 }
},
{
name: "Ghi",
className:
'${({ c }) => c && "flex"} ${({ a, b, c }) => (a && b) || c ? "justify-center" : "justify-start"} flex flex-col',
classNameOffsets: { start: 263, end: 359 }
'${({ c, a }) => c(a?.e) && "flex"} ${({ a, b, c }) => (a && b) || c ? "justify-center" : "justify-start"} flex flex-col',
classNameOffsets: { start: 266, end: 368 }
},
{
name: "Efg",
className: "justify-center",
classNameOffsets: { start: 378, end: 404 }
classNameOffsets: { start: 387, end: 413 }
},
{
name: "Ghi",
Expand Down Expand Up @@ -167,7 +167,7 @@ describe("generateDeclarations", () => {
exportIdentifier: false
});
expect(declarations).toEqual(
"const Abc = tw.div`flex flex-col`\n" +
"const Abc = tw.div`flex flex-col`\n\n" +
'const Xyz = tw.div`${({ c }) => c ? "justify-center" : "justify-start"}`'
);
});
Expand All @@ -191,7 +191,7 @@ describe("generateDeclarations", () => {
exportIdentifier: true
});
expect(declarations).toEqual(
"export const Abc = tw.div`flex flex-col`\n" +
"export const Abc = tw.div`flex flex-col`\n\n" +
'export const Xyz = tw.div`${({ c }) => c ? "justify-center" : "justify-start"}`'
);
});
Expand Down Expand Up @@ -238,7 +238,7 @@ describe("generateDeclarations", () => {
exportIdentifier: false
});
expect(declarations).toEqual(
"const Abc = tw.span`flex flex-col`\n" +
"const Abc = tw.span`flex flex-col`\n\n" +
'const Xyz = tw(Efg)`${({ c }) => c ? "justify-center" : "justify-start"}`'
);
});
Expand Down Expand Up @@ -285,7 +285,7 @@ describe("generateDeclarations", () => {
exportIdentifier: true
});
expect(declarations).toEqual(
"export const Abc = tw.span`flex flex-col`\n" +
"export const Abc = tw.span`flex flex-col`\n\n" +
'export const Xyz = tw(Efg)`${({ c }) => c ? "justify-center" : "justify-start"}`'
);
});
Expand Down
112 changes: 52 additions & 60 deletions src/lib/extractor.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import * as parser from "@babel/parser";
import traverse from "@babel/traverse";
import {
ConditionalExpression,
Expression,
isCallExpression,
isConditionalExpression,
isExpression,
isIdentifier,
isJSXAttribute,
isJSXClosingElement,
isJSXEmptyExpression,
isJSXIdentifier,
isJSXOpeningElement,
isLogicalExpression,
isMemberExpression,
isOptionalMemberExpression,
isStringLiteral,
isTemplateLiteral,
JSXAttribute,
JSXElement,
JSXOpeningElement,
Node,
LogicalExpression
Node
} from "@babel/types";

const parseOptions: parser.ParserOptions = {
Expand Down Expand Up @@ -174,50 +184,38 @@ const isComponent = (
const isDefined = (number: number | null | undefined): number is number =>
typeof number !== "undefined" && number !== null;

const fillLogicalExpressionIdentifiers = (
expression: LogicalExpression,
identifiers: string[]
) => {
if (expression.left.type === "Identifier") {
identifiers.push(expression.left.name);
} else if (expression.left.type === "LogicalExpression") {
fillLogicalExpressionIdentifiers(expression.left, identifiers);
}

if (expression.right.type === "Identifier") {
identifiers.push(expression.right.name);
} else if (expression.right.type === "LogicalExpression") {
fillLogicalExpressionIdentifiers(expression.right, identifiers);
}
};

const fillConditionalExpressionIdentifiers = (
expression: ConditionalExpression,
identifiers: string[]
) => {
if (expression.test.type === "Identifier") {
identifiers.push(expression.test.name);
} else if (expression.test.type === "LogicalExpression") {
fillLogicalExpressionIdentifiers(expression.test, identifiers);
}
return identifiers;
};

const fillExpressionIdentifiers = (
expression: ConditionalExpression | LogicalExpression,
expression: Expression,
identifiers: string[]
) => {
if (expression.type === "ConditionalExpression") {
fillConditionalExpressionIdentifiers(expression, identifiers);
} else if (expression.type === "LogicalExpression") {
fillLogicalExpressionIdentifiers(expression, identifiers);
if (isIdentifier(expression) && !identifiers.includes(expression.name)) {
identifiers.push(expression.name);
} else if (
isOptionalMemberExpression(expression) ||
isMemberExpression(expression)
) {
fillExpressionIdentifiers(expression.object, identifiers);
if (expression.computed && isExpression(expression.property)) {
fillExpressionIdentifiers(expression.property, identifiers);
}
} else if (isCallExpression(expression) && isExpression(expression.callee)) {
fillExpressionIdentifiers(expression.callee, identifiers);
for (const argument of expression.arguments) {
if (isExpression(argument)) {
fillExpressionIdentifiers(argument, identifiers);
}
}
} else if (isConditionalExpression(expression)) {
fillExpressionIdentifiers(expression.test, identifiers);
} else if (isLogicalExpression(expression)) {
fillExpressionIdentifiers(expression.left, identifiers);
fillExpressionIdentifiers(expression.right, identifiers);
} else if (!isStringLiteral(expression)) {
console.log("Unknown expression:", expression);
}
};

const buildExpressionText = (
code: string,
expression: ConditionalExpression | LogicalExpression
) => {
const buildExpressionText = (code: string, expression: Expression) => {
if (isDefined(expression.start) && isDefined(expression.end)) {
const identifiers: string[] = [];
fillExpressionIdentifiers(expression, identifiers);
Expand All @@ -229,13 +227,11 @@ const buildExpressionText = (
return "";
};

const extractClassNameAttribute = (jsxOpeningNode: JSXOpeningElement) => {
const attributes = jsxOpeningNode.attributes;
return attributes.find(
const extractClassNameAttribute = (jsxOpeningNode: JSXOpeningElement) =>
jsxOpeningNode.attributes.find(
attribute =>
attribute.type === "JSXAttribute" && attribute.name?.name === "className"
isJSXAttribute(attribute) && attribute.name.name === "className"
) as JSXAttribute | undefined;
};

const extractClassName = (
code: string,
Expand All @@ -247,27 +243,23 @@ const extractClassName = (
}
if (classNameAttribute.value.type === "JSXExpressionContainer") {
const expression = classNameAttribute.value.expression;
if (
expression.type === "ConditionalExpression" ||
expression.type === "LogicalExpression"
) {
return buildExpressionText(code, expression);
if (isJSXEmptyExpression(expression)) {
return "";
}
if (expression.type === "TemplateLiteral") {
const conditionalOrLogicalExpressions = expression.expressions.filter(
expr =>
expr.type === "ConditionalExpression" ||
expr.type === "LogicalExpression"
) as ConditionalExpression[];
const expressionText = conditionalOrLogicalExpressions
.map(expr => buildExpressionText(code, expr))

if (isTemplateLiteral(expression)) {
const expressionText = expression.expressions
.filter(expr => isExpression(expr))
.map(expr => buildExpressionText(code, expr as Expression))
.join(" ");
const quasisText = expression.quasis
.map(quasi => quasi.value.raw?.trim() || "")
.filter(Boolean)
.join(" ");
return `${expressionText} ${quasisText}`;
}

return buildExpressionText(code, expression);
}
return "";
};
Expand Down Expand Up @@ -403,7 +395,7 @@ export const generateDeclarations = ({
: `(${component.type})`
}\`${component.className}\``;
})
.join("\n");
.join("\n\n");

export const getUnderlyingComponent = (
code: string,
Expand Down

0 comments on commit b72f621

Please sign in to comment.