diff --git a/package.json b/package.json index 9d58fe2..9a2937f 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/babel-tailwind.ts b/src/babel-tailwind.ts index 64d6c31..0085402 100644 --- a/src/babel-tailwind.ts +++ b/src/babel-tailwind.ts @@ -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(({ 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"] + ) ); } diff --git a/src/index.test.ts b/src/index.test.ts index 4fb9913..ed48e3f 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -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 ( +
+ Hello, world! +
+ ); + } + `, + }); + + 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: {}, diff --git a/src/index.ts b/src/index.ts index d3ed4db..f240d23 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 {