Fix conditional expression

This commit is contained in:
Alex 2024-07-01 18:18:08 -04:00
parent 69cc90730c
commit 835c5b7810
6 changed files with 107 additions and 52 deletions

View File

@ -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": {

8
pnpm-lock.yaml generated
View File

@ -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: {}

View File

@ -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 = <K extends string | number, V, R>(
map: Record<K, V>,
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<string, string | object>,
(cls, key) =>
typeof cls === "string"
? trimPrefix(cls, `${modifier}-[${key}]:`)
: flatMapEntries(cls as Record<string, string>, (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!)
)
);
}

View File

@ -1,10 +1,17 @@
import cx from "clsx";
import { type FunctionComponent, forwardRef } from "react";
interface WithClassName<P = object> extends FunctionComponent<P> {
interface WithClassName<Props = object> extends FunctionComponent<Props> {
className: string;
}
type PresetClassNameValue = string | string[];
type PresetClassName<Props = UnknownProps> =
| PresetClassNameValue
| ((props: Props) => PresetClassNameValue);
type UnknownProps = Record<string, unknown>;
type InputProps = React.DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
@ -13,35 +20,38 @@ type InputProps = React.DetailedHTMLProps<
export const classed: {
(
type: "input",
className: string | string[],
className: PresetClassName<InputProps>,
defaultProps?: Partial<InputProps>
): InputProps;
<K extends keyof JSX.IntrinsicElements>(
type: K,
className: string | string[],
className: PresetClassName<JSX.IntrinsicElements[K]>,
defaultProps?: Partial<JSX.IntrinsicElements[K]>
): JSX.IntrinsicElements[K];
(
type: string,
className: string | string[],
defaultProps?: Record<string, unknown>
className: PresetClassName,
defaultProps?: UnknownProps
): WithClassName<React.ClassAttributes<any> & React.DOMAttributes<any>>;
<P>(
type: FunctionComponent<P>,
className: string | string[],
defaultProps?: Partial<P>
): WithClassName<P>;
<P>(
type: React.ComponentClass<P>,
className: string | string[],
defaultProps?: Partial<P>
): WithClassName<P>;
<Props>(
type: FunctionComponent<Props>,
className: PresetClassName<Props>,
defaultProps?: Partial<Props>
): WithClassName<Props>;
<Props>(
type: React.ComponentClass<Props>,
className: PresetClassName<Props>,
defaultProps?: Partial<Props>
): WithClassName<Props>;
} = (
Component: any,
classNameInput: string | string[],
defaultProps?: Record<string, unknown>
classNameInput: PresetClassName<any>,
defaultProps?: UnknownProps
) => {
const className = cx(classNameInput);
const className =
typeof classNameInput === "function"
? (props: any) => cx(classNameInput(props))
: () => cx(classNameInput);
const component: any = forwardRef<any, any>(({ className: cls, ...props }, ref) => (
<Component
{...defaultProps}
@ -49,8 +59,8 @@ export const classed: {
ref={ref}
className={
typeof cls === "function"
? (...args: unknown[]) => cx(className, cls(...args))
: cx(className, cls)
? (...args: unknown[]) => cx(className(props), cls(...args))
: cx(className(props), cls)
}
/>
));

View File

@ -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 (
<div css={isCenter ? "text-center" : undefined}>
Hello, world!
</div>
);
}
`,
});
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: {},

View File

@ -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 {