Support css functions
This commit is contained in:
parent
7072dc493d
commit
d0f032edc1
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@aet/tailwind",
|
"name": "@aet/tailwind",
|
||||||
"version": "0.0.1-beta.11",
|
"version": "0.0.1-beta.14",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { basename, dirname, extname, join } from "node:path";
|
import { basename, dirname, extname, join } from "node:path";
|
||||||
import type babel from "@babel/core";
|
import type babel from "@babel/core";
|
||||||
import hash from "@emotion/hash";
|
import hash from "@emotion/hash";
|
||||||
|
import { isPlainObject } from "lodash";
|
||||||
import { type NodePath, type types as t } from "@babel/core";
|
import { type NodePath, type types as t } from "@babel/core";
|
||||||
import type { SourceLocation, StyleMapEntry } from "./shared";
|
import type { SourceLocation, StyleMapEntry } from "./shared";
|
||||||
import { type ResolveTailwindOptions, getClassName } from "./index";
|
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 }) => ({
|
return definePlugin<BabelPluginState>(({ types: t }) => ({
|
||||||
Program: {
|
Program: {
|
||||||
enter(path, state) {
|
enter(path, state) {
|
||||||
@ -155,38 +184,10 @@ export function babelTailwind(
|
|||||||
|
|
||||||
const { node } = path;
|
const { node } = path;
|
||||||
|
|
||||||
const { callee, arguments: args } = node;
|
const { callee } = node;
|
||||||
if (!t.isIdentifier(callee, { name: macroFunction })) return;
|
if (!t.isIdentifier(callee, { name: macroFunction })) return;
|
||||||
|
|
||||||
const trimmed = args
|
const trimmed = evaluateArgs(path.get("arguments"));
|
||||||
.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 className = getClass(trimmed);
|
const className = getClass(trimmed);
|
||||||
recordIfAbsent({
|
recordIfAbsent({
|
||||||
key: className,
|
key: className,
|
||||||
@ -218,7 +219,7 @@ export function babelTailwind(
|
|||||||
const { value } = node;
|
const { value } = node;
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
const trimmed = value.replace(/\s+/g, " ").trim();
|
const trimmed = trim(value);
|
||||||
const className = getClass(trimmed);
|
const className = getClass(trimmed);
|
||||||
recordIfAbsent({
|
recordIfAbsent({
|
||||||
key: className,
|
key: className,
|
||||||
@ -233,6 +234,16 @@ export function babelTailwind(
|
|||||||
go(element);
|
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) {
|
JSXExpressionContainer(path) {
|
||||||
go(path.get("expression"));
|
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) {
|
if (classNameAttribute) {
|
||||||
const attrValue = classNameAttribute.value!;
|
const attrValue = classNameAttribute.value!;
|
||||||
const valuePathNode = valuePath.node;
|
|
||||||
const wrap = (originalValue: babel.types.Expression) =>
|
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 both are string literals, we can merge them directly here
|
||||||
if (t.isStringLiteral(attrValue) && t.isStringLiteral(valuePathNode)) {
|
if (t.isStringLiteral(attrValue) && t.isStringLiteral(valuePathNode)) {
|
||||||
@ -273,7 +294,10 @@ export function babelTailwind(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
parent.attributes.push(
|
parent.attributes.push(
|
||||||
t.jsxAttribute(t.jsxIdentifier("className"), valuePath.node)
|
t.jsxAttribute(
|
||||||
|
t.jsxIdentifier("className"),
|
||||||
|
valuePathNode as (typeof valuePath)["node"]
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,23 +159,26 @@ describe("babel-tailwind", () => {
|
|||||||
expect(files.css.text).toMatch(`.${clsName} {\n text-align: center;\n}`);
|
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({
|
const { files } = await compileESBuild({
|
||||||
clsx: "emotion",
|
clsx: "emotion",
|
||||||
expectFiles: 2,
|
expectFiles: 2,
|
||||||
javascript: /* tsx */ `
|
javascript: /* tsx */ `
|
||||||
export default tw("text-sm", {
|
export default tw("text-sm", \`flex\`, {
|
||||||
"group-hover": "text-center",
|
"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.js.text).toContain(`= "${clsName}"`);
|
||||||
expect(files.css.text).toMatch(
|
expect(files.css.text).toMatch(
|
||||||
[
|
[
|
||||||
`.${clsName} {`,
|
`.${clsName} {`,
|
||||||
|
" display: flex;",
|
||||||
" font-size: 0.875rem;",
|
" font-size: 0.875rem;",
|
||||||
" line-height: 1.25rem;",
|
" 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 () => {
|
it("supports importing tailwind/base", async () => {
|
||||||
const postcss = createPostCSS({
|
const postcss = createPostCSS({
|
||||||
tailwindConfig: {},
|
tailwindConfig: {},
|
||||||
|
@ -19,7 +19,7 @@ type GetClassName = (className: string) => string;
|
|||||||
*/
|
*/
|
||||||
export interface TailwindFunction {
|
export interface TailwindFunction {
|
||||||
(strings: TemplateStringsArray): string;
|
(strings: TemplateStringsArray): string;
|
||||||
(group: { [modifier: string]: string }): string;
|
(...args: (string | { [modifier: string]: string })[]): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TailwindPluginOptions {
|
export interface TailwindPluginOptions {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user