Fix conditional expression
This commit is contained in:
parent
69cc90730c
commit
835c5b7810
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@aet/tailwind",
|
"name": "@aet/tailwind",
|
||||||
"version": "0.0.1-beta.15",
|
"version": "0.0.1-beta.20",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -44,6 +44,7 @@
|
|||||||
"@emotion/hash": "^0.9.1",
|
"@emotion/hash": "^0.9.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
|
"tiny-invariant": "^1.3.3",
|
||||||
"type-fest": "^4.20.1"
|
"type-fest": "^4.20.1"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
|
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@ -20,6 +20,9 @@ importers:
|
|||||||
postcss:
|
postcss:
|
||||||
specifier: ^8.4.38
|
specifier: ^8.4.38
|
||||||
version: 8.4.38
|
version: 8.4.38
|
||||||
|
tiny-invariant:
|
||||||
|
specifier: ^1.3.3
|
||||||
|
version: 1.3.3
|
||||||
type-fest:
|
type-fest:
|
||||||
specifier: ^4.20.1
|
specifier: ^4.20.1
|
||||||
version: 4.20.1
|
version: 4.20.1
|
||||||
@ -1924,6 +1927,9 @@ packages:
|
|||||||
thenify@3.3.1:
|
thenify@3.3.1:
|
||||||
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
|
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
|
||||||
|
|
||||||
|
tiny-invariant@1.3.3:
|
||||||
|
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||||
|
|
||||||
tinybench@2.6.0:
|
tinybench@2.6.0:
|
||||||
resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==}
|
resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==}
|
||||||
|
|
||||||
@ -3998,6 +4004,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
any-promise: 1.3.0
|
any-promise: 1.3.0
|
||||||
|
|
||||||
|
tiny-invariant@1.3.3: {}
|
||||||
|
|
||||||
tinybench@2.6.0: {}
|
tinybench@2.6.0: {}
|
||||||
|
|
||||||
tinypool@0.8.3: {}
|
tinypool@0.8.3: {}
|
||||||
|
@ -2,6 +2,7 @@ import { basename, dirname, extname, join } from "node:path";
|
|||||||
import type babel from "@babel/core";
|
import type babel from "@babel/core";
|
||||||
import hash from "@emotion/hash";
|
import hash from "@emotion/hash";
|
||||||
import { isPlainObject } from "lodash";
|
import { isPlainObject } from "lodash";
|
||||||
|
import invariant from "tiny-invariant";
|
||||||
import { type NodePath, type types as t } from "@babel/core";
|
import { type NodePath, type types as t } from "@babel/core";
|
||||||
import type { SourceLocation, StyleMapEntry } from "./shared";
|
import type { SourceLocation, StyleMapEntry } from "./shared";
|
||||||
import { type ResolveTailwindOptions, getClassName } from "./index";
|
import { type ResolveTailwindOptions, getClassName } from "./index";
|
||||||
@ -37,6 +38,15 @@ interface BabelPluginState {
|
|||||||
export type ClassNameCollector = (path: string, entries: StyleMapEntry[]) => void;
|
export type ClassNameCollector = (path: string, entries: StyleMapEntry[]) => void;
|
||||||
|
|
||||||
const trim = (value: string) => value.replace(/\s+/g, " ").trim();
|
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(
|
export function babelTailwind(
|
||||||
{
|
{
|
||||||
@ -76,30 +86,31 @@ export function babelTailwind(
|
|||||||
return paths
|
return paths
|
||||||
.flatMap(path => {
|
.flatMap(path => {
|
||||||
const { confident, value } = path.evaluate();
|
const { confident, value } = path.evaluate();
|
||||||
if (!confident) {
|
invariant(confident, `${macroFunction} argument cannot be statically evaluated`);
|
||||||
throw new Error(`${macroFunction} argument cannot be statically evaluated`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
return trim(value);
|
return trim(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPlainObject(value)) {
|
if (isPlainObject(value)) {
|
||||||
return Object.entries(value).flatMap(([modifier, classes]) => {
|
return flatMapEntries(value, (classes, modifier) => {
|
||||||
if (modifier === "data" && isPlainObject(classes)) {
|
if (modifier === "data" && isPlainObject(classes)) {
|
||||||
return Object.entries(classes as object).flatMap(([key, cls]) =>
|
return flatMapEntries(
|
||||||
trim(cls)
|
classes as Record<string, string | object>,
|
||||||
.split(" ")
|
(cls, key) =>
|
||||||
.map(value => `${modifier}-[${key}]:${value}`)
|
typeof cls === "string"
|
||||||
|
? trimPrefix(cls, `${modifier}-[${key}]:`)
|
||||||
|
: flatMapEntries(cls as Record<string, string>, (cls, attrValue) =>
|
||||||
|
trimPrefix(cls, `${modifier}-[${key}=${attrValue}]:`)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof classes !== "string") {
|
invariant(
|
||||||
throw new Error(`Value for "${modifier}" should be a string`);
|
typeof classes === "string",
|
||||||
}
|
`Value for "${modifier}" should be a string`
|
||||||
return trim(classes)
|
);
|
||||||
.split(" ")
|
return trimPrefix(classes, modifier + ":");
|
||||||
.map(cls => modifier + ":" + cls);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,9 +149,7 @@ export function babelTailwind(
|
|||||||
|
|
||||||
exit({ node }, { filename, tailwindMap }) {
|
exit({ node }, { filename, tailwindMap }) {
|
||||||
if (!tailwindMap.size) return;
|
if (!tailwindMap.size) return;
|
||||||
if (!filename) {
|
invariant(filename, "babel: missing state.filename");
|
||||||
throw new Error("babel: missing state.filename");
|
|
||||||
}
|
|
||||||
|
|
||||||
const cssName = basename(filename, extname(filename)) + ".css";
|
const cssName = basename(filename, extname(filename)) + ".css";
|
||||||
|
|
||||||
@ -170,9 +179,10 @@ export function babelTailwind(
|
|||||||
} = node;
|
} = node;
|
||||||
if (!t.isIdentifier(tag, { name: macroFunction })) return;
|
if (!t.isIdentifier(tag, { name: macroFunction })) return;
|
||||||
|
|
||||||
if (expressions.length) {
|
invariant(
|
||||||
throw new Error(`${macroFunction}\`\` should not contain expressions`);
|
!expressions.length,
|
||||||
}
|
`${macroFunction}\`\` should not contain expressions`
|
||||||
|
);
|
||||||
|
|
||||||
const value = quasis[0].value.cooked;
|
const value = quasis[0].value.cooked;
|
||||||
if (value) {
|
if (value) {
|
||||||
@ -225,9 +235,8 @@ export function babelTailwind(
|
|||||||
StringLiteral(path) {
|
StringLiteral(path) {
|
||||||
const { node } = path;
|
const { node } = path;
|
||||||
const { value } = node;
|
const { value } = node;
|
||||||
|
|
||||||
if (value) {
|
|
||||||
const trimmed = trim(value);
|
const trimmed = trim(value);
|
||||||
|
if (trimmed) {
|
||||||
const className = getClass(trimmed);
|
const className = getClass(trimmed);
|
||||||
recordIfAbsent({
|
recordIfAbsent({
|
||||||
key: className,
|
key: className,
|
||||||
@ -304,7 +313,7 @@ export function babelTailwind(
|
|||||||
parent.attributes.push(
|
parent.attributes.push(
|
||||||
t.jsxAttribute(
|
t.jsxAttribute(
|
||||||
t.jsxIdentifier("className"),
|
t.jsxIdentifier("className"),
|
||||||
valuePathNode as (typeof valuePath)["node"]
|
t.jSXExpressionContainer(valuePathNode!)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
import cx from "clsx";
|
import cx from "clsx";
|
||||||
import { type FunctionComponent, forwardRef } from "react";
|
import { type FunctionComponent, forwardRef } from "react";
|
||||||
|
|
||||||
interface WithClassName<P = object> extends FunctionComponent<P> {
|
interface WithClassName<Props = object> extends FunctionComponent<Props> {
|
||||||
className: string;
|
className: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PresetClassNameValue = string | string[];
|
||||||
|
type PresetClassName<Props = UnknownProps> =
|
||||||
|
| PresetClassNameValue
|
||||||
|
| ((props: Props) => PresetClassNameValue);
|
||||||
|
|
||||||
|
type UnknownProps = Record<string, unknown>;
|
||||||
|
|
||||||
type InputProps = React.DetailedHTMLProps<
|
type InputProps = React.DetailedHTMLProps<
|
||||||
React.InputHTMLAttributes<HTMLInputElement>,
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
HTMLInputElement
|
HTMLInputElement
|
||||||
@ -13,35 +20,38 @@ type InputProps = React.DetailedHTMLProps<
|
|||||||
export const classed: {
|
export const classed: {
|
||||||
(
|
(
|
||||||
type: "input",
|
type: "input",
|
||||||
className: string | string[],
|
className: PresetClassName<InputProps>,
|
||||||
defaultProps?: Partial<InputProps>
|
defaultProps?: Partial<InputProps>
|
||||||
): InputProps;
|
): InputProps;
|
||||||
<K extends keyof JSX.IntrinsicElements>(
|
<K extends keyof JSX.IntrinsicElements>(
|
||||||
type: K,
|
type: K,
|
||||||
className: string | string[],
|
className: PresetClassName<JSX.IntrinsicElements[K]>,
|
||||||
defaultProps?: Partial<JSX.IntrinsicElements[K]>
|
defaultProps?: Partial<JSX.IntrinsicElements[K]>
|
||||||
): JSX.IntrinsicElements[K];
|
): JSX.IntrinsicElements[K];
|
||||||
(
|
(
|
||||||
type: string,
|
type: string,
|
||||||
className: string | string[],
|
className: PresetClassName,
|
||||||
defaultProps?: Record<string, unknown>
|
defaultProps?: UnknownProps
|
||||||
): WithClassName<React.ClassAttributes<any> & React.DOMAttributes<any>>;
|
): WithClassName<React.ClassAttributes<any> & React.DOMAttributes<any>>;
|
||||||
<P>(
|
<Props>(
|
||||||
type: FunctionComponent<P>,
|
type: FunctionComponent<Props>,
|
||||||
className: string | string[],
|
className: PresetClassName<Props>,
|
||||||
defaultProps?: Partial<P>
|
defaultProps?: Partial<Props>
|
||||||
): WithClassName<P>;
|
): WithClassName<Props>;
|
||||||
<P>(
|
<Props>(
|
||||||
type: React.ComponentClass<P>,
|
type: React.ComponentClass<Props>,
|
||||||
className: string | string[],
|
className: PresetClassName<Props>,
|
||||||
defaultProps?: Partial<P>
|
defaultProps?: Partial<Props>
|
||||||
): WithClassName<P>;
|
): WithClassName<Props>;
|
||||||
} = (
|
} = (
|
||||||
Component: any,
|
Component: any,
|
||||||
classNameInput: string | string[],
|
classNameInput: PresetClassName<any>,
|
||||||
defaultProps?: Record<string, unknown>
|
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) => (
|
const component: any = forwardRef<any, any>(({ className: cls, ...props }, ref) => (
|
||||||
<Component
|
<Component
|
||||||
{...defaultProps}
|
{...defaultProps}
|
||||||
@ -49,8 +59,8 @@ export const classed: {
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={
|
className={
|
||||||
typeof cls === "function"
|
typeof cls === "function"
|
||||||
? (...args: unknown[]) => cx(className, cls(...args))
|
? (...args: unknown[]) => cx(className(props), cls(...args))
|
||||||
: cx(className, cls)
|
: cx(className(props), cls)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
@ -169,13 +169,16 @@ describe("babel-tailwind", () => {
|
|||||||
"[&>div]": \`font-semibold\`,
|
"[&>div]": \`font-semibold\`,
|
||||||
data: {
|
data: {
|
||||||
"name='hello'": "text-right",
|
"name='hello'": "text-right",
|
||||||
|
nested: {
|
||||||
|
true: "border",
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const clsName = getClassName(
|
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.js.text).toContain(`= "${clsName}"`);
|
||||||
expect(files.css.text).toMatch(
|
expect(files.css.text).toMatch(
|
||||||
@ -188,6 +191,9 @@ describe("babel-tailwind", () => {
|
|||||||
`.group:hover .${clsName} {`,
|
`.group:hover .${clsName} {`,
|
||||||
" text-align: center;",
|
" text-align: center;",
|
||||||
"}",
|
"}",
|
||||||
|
`.${clsName}[data-nested=true] {`,
|
||||||
|
" border-width: 1px;",
|
||||||
|
"}",
|
||||||
`.${clsName}[data-name=hello] {`,
|
`.${clsName}[data-name=hello] {`,
|
||||||
" text-align: right;",
|
" 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 () => {
|
it("supports importing tailwind/base", async () => {
|
||||||
const postcss = createPostCSS({
|
const postcss = createPostCSS({
|
||||||
tailwindConfig: {},
|
tailwindConfig: {},
|
||||||
|
13
src/index.ts
13
src/index.ts
@ -13,18 +13,19 @@ export { createPostCSS } from "./shared";
|
|||||||
|
|
||||||
type GetClassName = (className: string) => string;
|
type GetClassName = (className: string) => string;
|
||||||
|
|
||||||
|
interface RecursiveStringObject {
|
||||||
|
[modifier: string]: string | RecursiveStringObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CSSAttributeValue = string | (string | RecursiveStringObject)[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 interface TailwindFunction {
|
export interface TailwindFunction {
|
||||||
(strings: TemplateStringsArray): string;
|
(strings: TemplateStringsArray): string;
|
||||||
(
|
(...args: (string | RecursiveStringObject)[]): string;
|
||||||
...args: (
|
|
||||||
| string
|
|
||||||
| ({ data?: { [key: string]: string } } & { [modifier: string]: string })
|
|
||||||
)[]
|
|
||||||
): string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TailwindPluginOptions {
|
export interface TailwindPluginOptions {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user