Support css functions

This commit is contained in:
Alex 2024-06-29 11:27:31 -04:00
parent 7072dc493d
commit d0f032edc1
4 changed files with 96 additions and 40 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@aet/tailwind",
"version": "0.0.1-beta.11",
"version": "0.0.1-beta.14",
"main": "dist/index.js",
"license": "MIT",
"scripts": {

View File

@ -1,6 +1,7 @@
import { basename, dirname, extname, join } from "node:path";
import type babel from "@babel/core";
import hash from "@emotion/hash";
import { isPlainObject } from "lodash";
import { type NodePath, type types as t } from "@babel/core";
import type { SourceLocation, StyleMapEntry } from "./shared";
import { type ResolveTailwindOptions, getClassName } from "./index";
@ -71,6 +72,34 @@ export function babelTailwind(
}
}
function evaluateArgs(paths: NodePath[]) {
return paths
.flatMap(path => {
const { confident, value } = path.evaluate();
if (!confident) {
throw new Error(`${macroFunction} argument cannot be statically evaluated`);
}
if (typeof value === "string") {
return trim(value);
}
if (isPlainObject(value)) {
return Object.entries(value).flatMap(([modifier, classes]) => {
if (typeof classes !== "string") {
throw new Error(`Value for "${modifier}" should be a string`);
}
return trim(classes)
.split(" ")
.map(cls => modifier + ":" + cls);
});
}
throw new Error(`${macroFunction} argument has an invalid type`);
})
.join(" ");
}
return definePlugin<BabelPluginState>(({ types: t }) => ({
Program: {
enter(path, state) {
@ -155,38 +184,10 @@ export function babelTailwind(
const { node } = path;
const { callee, arguments: args } = node;
const { callee } = node;
if (!t.isIdentifier(callee, { name: macroFunction })) return;
const trimmed = args
.flatMap((arg, i) => {
if (t.isStringLiteral(arg)) {
return trim(arg.value);
}
if (!t.isObjectExpression(arg)) {
throw new Error(`${macroFunction} should be called with an object literal`);
}
const ev = path.get("arguments")[i].evaluate();
if (!ev.confident || typeof ev.value !== "object" || ev.value == null) {
throw new Error(
`${macroFunction} should be called with a static object literal`
);
}
return Object.entries(ev.value).flatMap(([modifier, classes]) => {
if (typeof classes !== "string") {
throw new Error(`Value for "${modifier}" should be a string`);
}
return classes
.replace(/\s+/g, " ")
.trim()
.split(" ")
.map(cls => modifier + ":" + cls);
});
})
.join(" ");
const trimmed = evaluateArgs(path.get("arguments"));
const className = getClass(trimmed);
recordIfAbsent({
key: className,
@ -218,7 +219,7 @@ export function babelTailwind(
const { value } = node;
if (value) {
const trimmed = value.replace(/\s+/g, " ").trim();
const trimmed = trim(value);
const className = getClass(trimmed);
recordIfAbsent({
key: className,
@ -233,6 +234,16 @@ export function babelTailwind(
go(element);
}
},
ObjectExpression(path) {
const trimmed = evaluateArgs([path]);
const className = getClass(trimmed);
recordIfAbsent({
key: className,
className: trimmed,
location: sliceText(path.node),
});
path.replaceWith(t.stringLiteral(className));
},
JSXExpressionContainer(path) {
go(path.get("expression"));
},
@ -250,11 +261,21 @@ export function babelTailwind(
},
}));
let valuePathNode = extractJSXContainer(valuePath.node);
if (
t.isArrayExpression(valuePathNode) &&
valuePathNode.elements.every(node => t.isStringLiteral(node))
) {
valuePathNode = t.stringLiteral(
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
(valuePathNode.elements as t.StringLiteral[]).map(node => node.value).join(" ")
);
}
if (classNameAttribute) {
const attrValue = classNameAttribute.value!;
const valuePathNode = valuePath.node;
const wrap = (originalValue: babel.types.Expression) =>
t.callExpression(getCx(), [originalValue, extractJSXContainer(valuePathNode)]);
t.callExpression(getCx(), [originalValue, valuePathNode]);
// If both are string literals, we can merge them directly here
if (t.isStringLiteral(attrValue) && t.isStringLiteral(valuePathNode)) {
@ -273,7 +294,10 @@ export function babelTailwind(
}
} else {
parent.attributes.push(
t.jsxAttribute(t.jsxIdentifier("className"), valuePath.node)
t.jsxAttribute(
t.jsxIdentifier("className"),
valuePathNode as (typeof valuePath)["node"]
)
);
}

View File

@ -159,23 +159,26 @@ describe("babel-tailwind", () => {
expect(files.css.text).toMatch(`.${clsName} {\n text-align: center;\n}`);
});
it.only("supports grouped tw", async () => {
it("supports grouped tw", async () => {
const { files } = await compileESBuild({
clsx: "emotion",
expectFiles: 2,
javascript: /* tsx */ `
export default tw("text-sm", {
export default tw("text-sm", \`flex\`, {
"group-hover": "text-center",
"[&>div]": "font-semibold",
"[&>div]": \`font-semibold\`,
})
`,
});
const clsName = getClassName("text-sm group-hover:text-center [&>div]:font-semibold");
const clsName = getClassName(
"text-sm flex group-hover:text-center [&>div]:font-semibold"
);
expect(files.js.text).toContain(`= "${clsName}"`);
expect(files.css.text).toMatch(
[
`.${clsName} {`,
" display: flex;",
" font-size: 0.875rem;",
" line-height: 1.25rem;",
"}",
@ -189,6 +192,35 @@ describe("babel-tailwind", () => {
);
});
it("supports grouped array css jsx attribute like tw function", async () => {
const { files } = await compileESBuild({
clsx: "emotion",
expectFiles: 2,
javascript: /* tsx */ `
export function Hello() {
return (
<div css={["text-center", { hover: "font-semibold" }]}>
Hello, world!
</div>
);
}
`,
});
const clsName = ["text-center", "hover:font-semibold"].map(getClassName);
expect(files.js.text).toContain(`className: "${clsName.join(" ")}"`);
expect(files.css.text).toMatch(
[
`.${clsName[0]} {`,
" text-align: center;",
"}",
`.${clsName[1]}:hover {`,
" font-weight: 600;",
"}",
].join("\n")
);
});
it("supports importing tailwind/base", async () => {
const postcss = createPostCSS({
tailwindConfig: {},

View File

@ -19,7 +19,7 @@ type GetClassName = (className: string) => string;
*/
export interface TailwindFunction {
(strings: TemplateStringsArray): string;
(group: { [modifier: string]: string }): string;
(...args: (string | { [modifier: string]: string })[]): string;
}
export interface TailwindPluginOptions {