Fix conditional expression
This commit is contained in:
parent
69cc90730c
commit
835c5b7810
@ -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
8
pnpm-lock.yaml
generated
@ -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: {}
|
||||
|
@ -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!)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
/>
|
||||
));
|
||||
|
@ -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: {},
|
||||
|
13
src/index.ts
13
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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user