Support grouped tw

This commit is contained in:
Alex
2024-06-29 10:47:23 -04:00
parent 2a6df0dd4a
commit eaa888e920
7 changed files with 88 additions and 53 deletions

View File

@ -1,9 +1,8 @@
{
"name": "@aet/tailwind",
"version": "0.0.1-beta.8",
"version": "0.0.1-beta.10",
"main": "dist/index.js",
"license": "MIT",
"private": true,
"scripts": {
"build": "./scripts/index.ts",
"test": "vitest"
@ -12,7 +11,7 @@
"dist"
],
"devDependencies": {
"@aet/eslint-rules": "^0.0.33",
"@aet/eslint-rules": "^0.0.34",
"@types/babel__core": "^7.20.5",
"@types/bun": "^1.1.6",
"@types/dedent": "^0.7.2",
@ -55,4 +54,4 @@
"singleQuote": false,
"trailingComma": "es5"
}
}
}

40
pnpm-lock.yaml generated
View File

@ -25,8 +25,8 @@ importers:
version: 4.20.1
devDependencies:
'@aet/eslint-rules':
specifier: ^0.0.33
version: 0.0.33(eslint@8.57.0)(typescript@5.5.2)
specifier: ^0.0.34
version: 0.0.34(eslint@8.57.0)(typescript@5.5.2)
'@types/babel__core':
specifier: ^7.20.5
version: 7.20.5
@ -107,8 +107,8 @@ packages:
resolution: {integrity: sha512-CpHRfl17loRfmX2yimeFiu72vTG/m+h8Trq2z90yAZr6rGP7kcV/ypsm56cZ/BrK8iK94MWNdsRa3QIgw1Xcrw==}
engines: {node: '>=18.0.0', npm: '>=9.0.0'}
'@aet/eslint-rules@0.0.33':
resolution: {integrity: sha512-ti3g5iLm3Jsegwd5RGgaY+8aWRUqnq+ErjyWueOiN/xf1pCAM9HSQ7cCv6Sq4kQlKrLZS7cGf6pwpy2lZZc5Dw==}
'@aet/eslint-rules@0.0.34':
resolution: {integrity: sha512-3NYOYf1HJg9IRQgvl0kGR6CZlfPzRk/rf8519xOD1bXdmChhpPP+MAHm5G1WKQqAVeAa4J6aV7hKy2iyJcHZTA==}
peerDependencies:
eslint: ^8.57.0
typescript: ^5.4.4
@ -121,10 +121,6 @@ packages:
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
'@babel/code-frame@7.24.2':
resolution: {integrity: sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==}
engines: {node: '>=6.9.0'}
'@babel/code-frame@7.24.7':
resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==}
engines: {node: '>=6.9.0'}
@ -199,10 +195,6 @@ packages:
resolution: {integrity: sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==}
engines: {node: '>=6.9.0'}
'@babel/highlight@7.24.2':
resolution: {integrity: sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==}
engines: {node: '>=6.9.0'}
'@babel/highlight@7.24.7':
resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==}
engines: {node: '>=6.9.0'}
@ -630,10 +622,6 @@ packages:
resolution: {integrity: sha512-mL7zNEOQybo5R3AavY+Am7KLv8BorIv7HCYS5rKoNZKQD9tsfGUpO4KdAn3sSUvTiS4PQkr2+K0KJbxj8H9NDg==}
engines: {node: ^18.18.0 || >=20.0.0}
'@typescript-eslint/types@7.9.0':
resolution: {integrity: sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w==}
engines: {node: ^18.18.0 || >=20.0.0}
'@typescript-eslint/typescript-estree@7.14.1':
resolution: {integrity: sha512-k5d0VuxViE2ulIO6FbxxSZaxqDVUyMbXcidC8rHvii0I56XZPv8cq+EhMns+d/EVIL41sMXqRbK3D10Oza1bbA==}
engines: {node: ^18.18.0 || >=20.0.0}
@ -2166,7 +2154,7 @@ snapshots:
'@aet/eslint-define-config@0.1.0-beta.1': {}
'@aet/eslint-rules@0.0.33(eslint@8.57.0)(typescript@5.5.2)':
'@aet/eslint-rules@0.0.34(eslint@8.57.0)(typescript@5.5.2)':
dependencies:
'@aet/eslint-define-config': 0.1.0-beta.1
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
@ -2218,11 +2206,6 @@ snapshots:
'@jridgewell/gen-mapping': 0.3.5
'@jridgewell/trace-mapping': 0.3.25
'@babel/code-frame@7.24.2':
dependencies:
'@babel/highlight': 7.24.2
picocolors: 1.0.0
'@babel/code-frame@7.24.7':
dependencies:
'@babel/highlight': 7.24.7
@ -2322,13 +2305,6 @@ snapshots:
'@babel/template': 7.24.7
'@babel/types': 7.24.7
'@babel/highlight@7.24.2':
dependencies:
'@babel/helper-validator-identifier': 7.24.7
chalk: 2.4.2
js-tokens: 4.0.0
picocolors: 1.0.0
'@babel/highlight@7.24.7':
dependencies:
'@babel/helper-validator-identifier': 7.24.7
@ -2387,7 +2363,7 @@ snapshots:
dependencies:
'@types/eslint': 8.56.10
'@types/estree': 1.0.5
'@typescript-eslint/types': 7.9.0
'@typescript-eslint/types': 7.14.1
comment-parser: 1.4.1
esquery: 1.5.0
jsdoc-type-pratt-parser: 4.0.0
@ -2717,8 +2693,6 @@ snapshots:
'@typescript-eslint/types@7.14.1': {}
'@typescript-eslint/types@7.9.0': {}
'@typescript-eslint/typescript-estree@7.14.1(typescript@5.5.2)':
dependencies:
'@typescript-eslint/types': 7.14.1
@ -3688,7 +3662,7 @@ snapshots:
parse-json@5.2.0:
dependencies:
'@babel/code-frame': 7.24.2
'@babel/code-frame': 7.24.7
error-ex: 1.3.2
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4

View File

@ -11,9 +11,6 @@ const tsupConfig = defineConfig({
dts: true,
treeshake: true,
platform: "node",
define: {
"process.env.BABEL_TAILWIND_BUILD": "true",
},
banner: {
js: "/* eslint-disable */",
},

View File

@ -40,7 +40,7 @@ export function babelTailwind(
styleMap,
clsx,
getClassName: getClass = getClassName,
taggedTemplateName,
macroFunction,
jsxAttributeAction = "delete",
jsxAttributeName = "css",
vite,
@ -122,17 +122,17 @@ export function babelTailwind(
},
TaggedTemplateExpression(path, { sliceText, recordIfAbsent }) {
if (taggedTemplateName == null) return;
if (macroFunction == null) return;
const { node } = path;
const {
tag,
quasi: { quasis, expressions },
} = node;
if (!t.isIdentifier(tag, { name: taggedTemplateName })) return;
if (!t.isIdentifier(tag, { name: macroFunction })) return;
if (expressions.length) {
throw new Error(`${taggedTemplateName}\`\` should not contain expressions`);
throw new Error(`${macroFunction}\`\` should not contain expressions`);
}
const value = quasis[0].value.cooked;
@ -148,6 +148,50 @@ export function babelTailwind(
}
},
CallExpression(path, { sliceText, recordIfAbsent }) {
if (macroFunction == null) return;
const { node } = path;
const { callee, arguments: args } = node;
if (!t.isIdentifier(callee, { name: macroFunction })) return;
if (args.length !== 1) {
throw new Error(`${macroFunction} should be called with exactly one argument`);
}
const [arg] = args;
if (!t.isObjectExpression(arg)) {
throw new Error(`${macroFunction} should be called with an object literal`);
}
const ev = path.get("arguments")[0].evaluate();
if (!ev.confident || typeof ev.value !== "object" || ev.value == null) {
throw new Error(`${macroFunction} should be called with a static object literal`);
}
const trimmed = 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);
recordIfAbsent({
key: className,
className: trimmed,
location: sliceText(node),
});
path.replaceWith(t.stringLiteral(className));
},
JSXAttribute(path, { sliceText, recordIfAbsent, getCx }) {
const { name } = path.node;
if (name.name !== jsxAttributeName) return;

View File

@ -159,6 +159,25 @@ describe("babel-tailwind", () => {
expect(files.css.text).toMatch(`.${clsName} {\n text-align: center;\n}`);
});
it("supports grouped tw", async () => {
const { files } = await compileESBuild({
clsx: "emotion",
expectFiles: 2,
javascript: /* tsx */ `
export default tw({
"group-hover": "text-center",
"[&>div]": "font-semibold",
})
`,
});
const clsName = getClassName("group-hover:text-center [&>div]:font-semibold");
expect(files.js.text).toContain(`= "${clsName}"`);
expect(files.css.text).toMatch(
`.group:hover .${clsName} {\n text-align: center;\n}\n.${clsName} > div {\n font-weight: 600;\n}`
);
});
it("supports importing tailwind/base", async () => {
const postcss = createPostCSS({
tailwindConfig: {},
@ -202,6 +221,7 @@ async function compileESBuild({
}) {
const tailwind = getTailwindPlugins({
tailwindConfig: {},
macroFunction: "tw",
...options,
});
const result = await esbuild.build({

View File

@ -17,7 +17,10 @@ type GetClassName = (className: string) => string;
* Tagged template macro function for Tailwind classes
* @example "tw" => tw`p-2 text-center`
*/
export type TaggedTailwindFunction = (strings: TemplateStringsArray) => string;
export interface TailwindFunction {
(strings: TemplateStringsArray): string;
(group: { [modifier: string]: string }): string;
}
export interface TailwindPluginOptions {
/**
@ -53,13 +56,13 @@ export interface TailwindPluginOptions {
jsxAttributeAction?: "delete" | "preserve" | ["rename", string];
/**
* Tagged template macro function to use for Tailwind classes
* Template macro function to use for Tailwind classes
* @default "tw"
* @example
* declare const tw: TaggedTailwindFunction;
* declare const tw: TailwindFunction;
* "tw" => tw`p-2 text-center`
*/
taggedTemplateName?: string | undefined;
macroFunction?: string | undefined;
/**
* The prefix to use for the generated class names.
@ -98,7 +101,7 @@ export type ResolveTailwindOptions = SetRequired<
| "postCSSPlugins"
| "styleMap"
| "tailwindConfig"
| "taggedTemplateName"
| "macroFunction"
>;
/**
@ -130,7 +133,7 @@ export function getTailwindPlugins(options: TailwindPluginOptions) {
jsxAttributeName: "css",
postCSSPlugins: [],
styleMap: new Map(),
taggedTemplateName: undefined,
macroFunction: undefined,
tailwindConfig: {},
...options,
};

View File

@ -2,9 +2,7 @@ import tailwind from "tailwindcss";
import postcss from "postcss";
import type { ResolveTailwindOptions } from "./index";
export const { name: pkgName } = [require][0](
process.env.BABEL_TAILWIND_BUILD ? "./package.json" : "../package.json"
);
export const { name: pkgName } = [require][0]("../package.json");
interface LineColumn {
line: number;