From 835c5b7810a823f4ec6daabd6d92d30e39bea51b Mon Sep 17 00:00:00 2001 From: Alex <8125011+alex-kinokon@users.noreply.github.com> Date: Mon, 1 Jul 2024 18:18:08 -0400 Subject: [PATCH] Fix conditional expression --- package.json | 3 ++- pnpm-lock.yaml | 8 ++++++ src/babel-tailwind.ts | 57 +++++++++++++++++++++++++------------------ src/classed.tsx | 50 ++++++++++++++++++++++--------------- src/index.test.ts | 28 ++++++++++++++++++++- src/index.ts | 13 +++++----- 6 files changed, 107 insertions(+), 52 deletions(-) diff --git a/package.json b/package.json index e2336aa..29c6b81 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aet/tailwind", - "version": "0.0.1-beta.15", + "version": "0.0.1-beta.20", "main": "dist/index.js", "license": "MIT", "scripts": { @@ -44,6 +44,7 @@ "@emotion/hash": "^0.9.1", "lodash": "^4.17.21", "postcss": "^8.4.38", + "tiny-invariant": "^1.3.3", "type-fest": "^4.20.1" }, "prettier": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 911fe6b..3d51c51 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: postcss: specifier: ^8.4.38 version: 8.4.38 + tiny-invariant: + specifier: ^1.3.3 + version: 1.3.3 type-fest: specifier: ^4.20.1 version: 4.20.1 @@ -1924,6 +1927,9 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.6.0: resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==} @@ -3998,6 +4004,8 @@ snapshots: dependencies: any-promise: 1.3.0 + tiny-invariant@1.3.3: {} + tinybench@2.6.0: {} tinypool@0.8.3: {} diff --git a/src/babel-tailwind.ts b/src/babel-tailwind.ts index 2f031cd..e78250a 100644 --- a/src/babel-tailwind.ts +++ b/src/babel-tailwind.ts @@ -2,6 +2,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 invariant from "tiny-invariant"; import { type NodePath, type types as t } from "@babel/core"; import type { SourceLocation, StyleMapEntry } from "./shared"; import { type ResolveTailwindOptions, getClassName } from "./index"; @@ -37,6 +38,15 @@ interface BabelPluginState { export type ClassNameCollector = (path: string, entries: StyleMapEntry[]) => void; const trim = (value: string) => value.replace(/\s+/g, " ").trim(); +const trimPrefix = (cls: string, prefix: string) => + trim(cls) + .split(" ") + .map(value => prefix + value); + +const flatMapEntries = ( + map: Record, + fn: (value: V, key: K) => R[] +): R[] => Object.entries(map).flatMap(([key, value]) => fn(value as V, key as K)); export function babelTailwind( { @@ -76,30 +86,31 @@ export function babelTailwind( return paths .flatMap(path => { const { confident, value } = path.evaluate(); - if (!confident) { - throw new Error(`${macroFunction} argument cannot be statically evaluated`); - } + invariant(confident, `${macroFunction} argument cannot be statically evaluated`); if (typeof value === "string") { return trim(value); } if (isPlainObject(value)) { - return Object.entries(value).flatMap(([modifier, classes]) => { + return flatMapEntries(value, (classes, modifier) => { if (modifier === "data" && isPlainObject(classes)) { - return Object.entries(classes as object).flatMap(([key, cls]) => - trim(cls) - .split(" ") - .map(value => `${modifier}-[${key}]:${value}`) + return flatMapEntries( + classes as Record, + (cls, key) => + typeof cls === "string" + ? trimPrefix(cls, `${modifier}-[${key}]:`) + : flatMapEntries(cls as Record, (cls, attrValue) => + trimPrefix(cls, `${modifier}-[${key}=${attrValue}]:`) + ) ); } - if (typeof classes !== "string") { - throw new Error(`Value for "${modifier}" should be a string`); - } - return trim(classes) - .split(" ") - .map(cls => modifier + ":" + cls); + invariant( + typeof classes === "string", + `Value for "${modifier}" should be a string` + ); + return trimPrefix(classes, modifier + ":"); }); } @@ -138,9 +149,7 @@ export function babelTailwind( exit({ node }, { filename, tailwindMap }) { if (!tailwindMap.size) return; - if (!filename) { - throw new Error("babel: missing state.filename"); - } + invariant(filename, "babel: missing state.filename"); const cssName = basename(filename, extname(filename)) + ".css"; @@ -170,9 +179,10 @@ export function babelTailwind( } = node; if (!t.isIdentifier(tag, { name: macroFunction })) return; - if (expressions.length) { - throw new Error(`${macroFunction}\`\` should not contain expressions`); - } + invariant( + !expressions.length, + `${macroFunction}\`\` should not contain expressions` + ); const value = quasis[0].value.cooked; if (value) { @@ -225,9 +235,8 @@ export function babelTailwind( StringLiteral(path) { const { node } = path; const { value } = node; - - if (value) { - const trimmed = trim(value); + const trimmed = trim(value); + if (trimmed) { const className = getClass(trimmed); recordIfAbsent({ key: className, @@ -304,7 +313,7 @@ export function babelTailwind( parent.attributes.push( t.jsxAttribute( t.jsxIdentifier("className"), - valuePathNode as (typeof valuePath)["node"] + t.jSXExpressionContainer(valuePathNode!) ) ); } diff --git a/src/classed.tsx b/src/classed.tsx index 97099b6..abe5b08 100644 --- a/src/classed.tsx +++ b/src/classed.tsx @@ -1,10 +1,17 @@ import cx from "clsx"; import { type FunctionComponent, forwardRef } from "react"; -interface WithClassName

extends FunctionComponent

{ +interface WithClassName extends FunctionComponent { className: string; } +type PresetClassNameValue = string | string[]; +type PresetClassName = + | PresetClassNameValue + | ((props: Props) => PresetClassNameValue); + +type UnknownProps = Record; + type InputProps = React.DetailedHTMLProps< React.InputHTMLAttributes, HTMLInputElement @@ -13,35 +20,38 @@ type InputProps = React.DetailedHTMLProps< export const classed: { ( type: "input", - className: string | string[], + className: PresetClassName, defaultProps?: Partial ): InputProps; ( type: K, - className: string | string[], + className: PresetClassName, defaultProps?: Partial ): JSX.IntrinsicElements[K]; ( type: string, - className: string | string[], - defaultProps?: Record + className: PresetClassName, + defaultProps?: UnknownProps ): WithClassName & React.DOMAttributes>; -

( - type: FunctionComponent

, - className: string | string[], - defaultProps?: Partial

- ): WithClassName

; -

( - type: React.ComponentClass

, - className: string | string[], - defaultProps?: Partial

- ): WithClassName

; + ( + type: FunctionComponent, + className: PresetClassName, + defaultProps?: Partial + ): WithClassName; + ( + type: React.ComponentClass, + className: PresetClassName, + defaultProps?: Partial + ): WithClassName; } = ( Component: any, - classNameInput: string | string[], - defaultProps?: Record + classNameInput: PresetClassName, + defaultProps?: UnknownProps ) => { - const className = cx(classNameInput); + const className = + typeof classNameInput === "function" + ? (props: any) => cx(classNameInput(props)) + : () => cx(classNameInput); const component: any = forwardRef(({ className: cls, ...props }, ref) => ( cx(className, cls(...args)) - : cx(className, cls) + ? (...args: unknown[]) => cx(className(props), cls(...args)) + : cx(className(props), cls) } /> )); diff --git a/src/index.test.ts b/src/index.test.ts index 2daa6e0..da1f0ac 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -169,13 +169,16 @@ describe("babel-tailwind", () => { "[&>div]": \`font-semibold\`, data: { "name='hello'": "text-right", + nested: { + true: "border", + } }, }) `, }); const clsName = getClassName( - "text-sm flex group-hover:text-center [&>div]:font-semibold data-[name='hello']:text-right" + "text-sm flex group-hover:text-center [&>div]:font-semibold data-[name='hello']:text-right data-[nested=true]:border" ); expect(files.js.text).toContain(`= "${clsName}"`); expect(files.css.text).toMatch( @@ -188,6 +191,9 @@ describe("babel-tailwind", () => { `.group:hover .${clsName} {`, " text-align: center;", "}", + `.${clsName}[data-nested=true] {`, + " border-width: 1px;", + "}", `.${clsName}[data-name=hello] {`, " text-align: right;", "}", @@ -227,6 +233,26 @@ describe("babel-tailwind", () => { ); }); + it('supports conditional expression in "css" attribute', async () => { + const { files } = await compileESBuild({ + clsx: "emotion", + expectFiles: 2, + javascript: /* tsx */ ` + export function Hello({ isCenter }) { + return ( +

+ Hello, world! +
+ ); + } + `, + }); + + const clsName = getClassName("text-center"); + expect(files.js.text).toContain(`className: isCenter ? "${clsName}" : void 0`); + expect(files.css.text).toMatch(`.${clsName} {\n text-align: center;\n}`); + }); + it("supports importing tailwind/base", async () => { const postcss = createPostCSS({ tailwindConfig: {}, diff --git a/src/index.ts b/src/index.ts index 3c06b9b..96851fe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,18 +13,19 @@ export { createPostCSS } from "./shared"; type GetClassName = (className: string) => string; +interface RecursiveStringObject { + [modifier: string]: string | RecursiveStringObject; +} + +export type CSSAttributeValue = string | (string | RecursiveStringObject)[]; + /** * Tagged template macro function for Tailwind classes * @example "tw" => tw`p-2 text-center` */ export interface TailwindFunction { (strings: TemplateStringsArray): string; - ( - ...args: ( - | string - | ({ data?: { [key: string]: string } } & { [modifier: string]: string }) - )[] - ): string; + (...args: (string | RecursiveStringObject)[]): string; } export interface TailwindPluginOptions {