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

40
pnpm-lock.yaml generated
View File

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

View File

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

View File

@ -40,7 +40,7 @@ export function babelTailwind(
styleMap, styleMap,
clsx, clsx,
getClassName: getClass = getClassName, getClassName: getClass = getClassName,
taggedTemplateName, macroFunction,
jsxAttributeAction = "delete", jsxAttributeAction = "delete",
jsxAttributeName = "css", jsxAttributeName = "css",
vite, vite,
@ -122,17 +122,17 @@ export function babelTailwind(
}, },
TaggedTemplateExpression(path, { sliceText, recordIfAbsent }) { TaggedTemplateExpression(path, { sliceText, recordIfAbsent }) {
if (taggedTemplateName == null) return; if (macroFunction == null) return;
const { node } = path; const { node } = path;
const { const {
tag, tag,
quasi: { quasis, expressions }, quasi: { quasis, expressions },
} = node; } = node;
if (!t.isIdentifier(tag, { name: taggedTemplateName })) return; if (!t.isIdentifier(tag, { name: macroFunction })) return;
if (expressions.length) { 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; 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 }) { JSXAttribute(path, { sliceText, recordIfAbsent, getCx }) {
const { name } = path.node; const { name } = path.node;
if (name.name !== jsxAttributeName) return; 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}`); 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 () => { it("supports importing tailwind/base", async () => {
const postcss = createPostCSS({ const postcss = createPostCSS({
tailwindConfig: {}, tailwindConfig: {},
@ -202,6 +221,7 @@ async function compileESBuild({
}) { }) {
const tailwind = getTailwindPlugins({ const tailwind = getTailwindPlugins({
tailwindConfig: {}, tailwindConfig: {},
macroFunction: "tw",
...options, ...options,
}); });
const result = await esbuild.build({ const result = await esbuild.build({

View File

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

View File

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