Compare commits
4 Commits
17f9527ab4
...
main
Author | SHA1 | Date | |
---|---|---|---|
1d0a8a1c36 | |||
bd683df539 | |||
52b19b3b36 | |||
8e41208a14 |
@ -3,6 +3,8 @@
|
|||||||
Compile-run Tailwind compiler.
|
Compile-run Tailwind compiler.
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
|
/// <reference types="@aet/tailwind/react-env" />
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return <div css="flex m-0"></div>;
|
return <div css="flex m-0"></div>;
|
||||||
}
|
}
|
||||||
|
51
package.json
51
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@aet/tailwind",
|
"name": "@aet/tailwind",
|
||||||
"version": "1.0.25",
|
"version": "1.0.35",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -14,7 +14,7 @@
|
|||||||
"exports": {
|
"exports": {
|
||||||
".": "./dist/index.js",
|
".": "./dist/index.js",
|
||||||
"./package.json": "./package.json",
|
"./package.json": "./package.json",
|
||||||
"./classed": "./dist/classed.js",
|
"./utils": "./dist/utils.js",
|
||||||
"./css-to-js": "./dist/css-to-js.js",
|
"./css-to-js": "./dist/css-to-js.js",
|
||||||
"./base": {
|
"./base": {
|
||||||
"types": "./dist/base.d.ts"
|
"types": "./dist/base.d.ts"
|
||||||
@ -30,49 +30,55 @@
|
|||||||
"./plugin/typography": "./dist/vendor/typography.js",
|
"./plugin/typography": "./dist/vendor/typography.js",
|
||||||
"./macro": {
|
"./macro": {
|
||||||
"types": "./dist/macro.d.ts"
|
"types": "./dist/macro.d.ts"
|
||||||
|
},
|
||||||
|
"./react-env": {
|
||||||
|
"types": "./dist/react-env.d.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@aet/eslint-rules": "2.0.36",
|
"@aet/eslint-rules": "^2.0.52",
|
||||||
"@types/babel__core": "^7.20.5",
|
"@types/babel__core": "^7.20.5",
|
||||||
"@types/bun": "^1.2.2",
|
"@types/bun": "^1.2.16",
|
||||||
"@types/dedent": "^0.7.2",
|
"@types/dedent": "^0.7.2",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^22.13.1",
|
"@types/node": "^24.0.1",
|
||||||
"@types/postcss-safe-parser": "^5.0.4",
|
"@types/postcss-safe-parser": "^5.0.4",
|
||||||
"@types/react": "^19.0.8",
|
"@types/react": "^19.1.8",
|
||||||
"@types/stylis": "^4.2.7",
|
"@types/stylis": "^4.2.7",
|
||||||
|
"@vitejs/plugin-react": "^4.5.2",
|
||||||
"cli-highlight": "^2.1.11",
|
"cli-highlight": "^2.1.11",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"colord": "^2.9.3",
|
"colord": "^2.9.3",
|
||||||
"css-what": "^6.1.0",
|
"css-what": "^6.1.0",
|
||||||
"dedent": "^1.5.3",
|
"dedent": "^1.6.0",
|
||||||
"esbuild-register": "^3.6.0",
|
"esbuild-register": "^3.6.0",
|
||||||
"eslint": "^9.19.0",
|
"eslint": "^9.28.0",
|
||||||
"nolyfill": "^1.0.43",
|
"nolyfill": "^1.0.44",
|
||||||
"postcss-nested": "^7.0.2",
|
"postcss-nested": "^7.0.2",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.5.3",
|
||||||
|
"react-refresh": "^0.17.0",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"tsup": "^8.3.6",
|
"tsup": "^8.5.0",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^6.1.0",
|
"vite": "^6.3.5",
|
||||||
"vitest": "^3.0.5"
|
"vitest": "^3.2.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"tailwindcss": "^3.4.17"
|
"tailwindcss": "^3.4.17"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.26.7",
|
"@babel/core": "^7.27.4",
|
||||||
"@emotion/hash": "^0.9.2",
|
"@emotion/hash": "^0.9.2",
|
||||||
"esbuild": "^0.24.2",
|
"clsx": "^2.1.1",
|
||||||
|
"esbuild": "^0.25.5",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"postcss": "^8.5.1",
|
"postcss": "^8.5.5",
|
||||||
"postcss-selector-parser": "^7.0.0",
|
"postcss-selector-parser": "^7.1.0",
|
||||||
"stylis": "^4.3.5",
|
"stylis": "^4.3.6",
|
||||||
"tiny-invariant": "^1.3.3",
|
"tiny-invariant": "^1.3.3",
|
||||||
"type-fest": "^4.33.0"
|
"type-fest": "^4.41.0"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"arrowParens": "avoid",
|
"arrowParens": "avoid",
|
||||||
@ -86,6 +92,9 @@
|
|||||||
"overrides": {
|
"overrides": {
|
||||||
"is-core-module": "npm:@nolyfill/is-core-module@^1",
|
"is-core-module": "npm:@nolyfill/is-core-module@^1",
|
||||||
"@babel/types": "7.26.7"
|
"@babel/types": "7.26.7"
|
||||||
}
|
},
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"esbuild"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
2839
pnpm-lock.yaml
generated
2839
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,6 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env tsx
|
||||||
import { promises as fs } from "node:fs";
|
import { promises as fs } from "node:fs";
|
||||||
|
|
||||||
import { pick } from "lodash-es";
|
|
||||||
import { build, defineConfig } from "tsup";
|
import { build, defineConfig } from "tsup";
|
||||||
|
|
||||||
import pkg from "../package.json" with { type: "json" };
|
import pkg from "../package.json" with { type: "json" };
|
||||||
@ -24,7 +23,7 @@ const tsupConfig = defineConfig({
|
|||||||
|
|
||||||
await build({
|
await build({
|
||||||
...tsupConfig,
|
...tsupConfig,
|
||||||
entry: ["src/classed.tsx", "src/css-to-js.ts"],
|
entry: ["src/utils.tsx", "src/css-to-js.ts"],
|
||||||
outDir: "dist",
|
outDir: "dist",
|
||||||
external: ["react", "react/jsx-runtime", "clsx"],
|
external: ["react", "react/jsx-runtime", "clsx"],
|
||||||
clean: true,
|
clean: true,
|
||||||
@ -33,7 +32,7 @@ await build({
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
build({
|
build({
|
||||||
...tsupConfig,
|
...tsupConfig,
|
||||||
entry: ["src/classed.tsx", "src/css-to-js.ts"],
|
entry: ["src/utils.tsx", "src/css-to-js.ts"],
|
||||||
outDir: "dist",
|
outDir: "dist",
|
||||||
external: ["react", "react/jsx-runtime", "clsx"],
|
external: ["react", "react/jsx-runtime", "clsx"],
|
||||||
}),
|
}),
|
||||||
@ -57,29 +56,17 @@ await Promise.all([
|
|||||||
external: ["tailwindcss/plugin", "tailwindcss/colors", "tailwindcss/defaultTheme"],
|
external: ["tailwindcss/plugin", "tailwindcss/colors", "tailwindcss/defaultTheme"],
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
Bun.write(
|
|
||||||
"dist/package.json",
|
|
||||||
JSON.stringify(
|
|
||||||
pick(pkg, [
|
|
||||||
"name",
|
|
||||||
"version",
|
|
||||||
"type",
|
|
||||||
"license",
|
|
||||||
"dependencies",
|
|
||||||
"author",
|
|
||||||
"exports",
|
|
||||||
]),
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
).replaceAll("./dist/", "./")
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
fs.copyFile("README.md", "dist/README.md"),
|
fs.copyFile("README.md", "dist/README.md"),
|
||||||
fs.copyFile("LICENSE.md", "dist/LICENSE.md"),
|
fs.copyFile("LICENSE.md", "dist/LICENSE.md"),
|
||||||
fs.copyFile("src/macro.d.ts", "dist/macro.d.ts"),
|
fs.copyFile("src/macro.d.ts", "dist/macro.d.ts"),
|
||||||
Bun.write(`dist/base.d.ts`, `/**\n * \`@tailwind base\` component.\n */\nexport {};`),
|
fs.copyFile("src/react-env.d.ts", "dist/react-env.d.ts"),
|
||||||
|
fs.writeFile(
|
||||||
|
`dist/base.d.ts`,
|
||||||
|
`/**\n * \`@tailwind base\` component.\n */\nexport {};`
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
33
src/__tests__/__snapshots__/group.test.ts.snap
Normal file
33
src/__tests__/__snapshots__/group.test.ts.snap
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`group > supports group 1`] = `
|
||||||
|
"import { jsx, jsxs } from "react/jsx-runtime";
|
||||||
|
function Hello() {
|
||||||
|
return /* @__PURE__ */ jsx("li", { className: "tw-1d1woxu", children: /* @__PURE__ */ jsxs("a", { href: "tel:{person.phone}", className: "tw-gbesv1", children: [
|
||||||
|
/* @__PURE__ */ jsx("span", { className: "tw-1psr9tm", children: "Call" }),
|
||||||
|
/* @__PURE__ */ jsx("svg", { className: "tw-f3p2y0" })
|
||||||
|
] }) });
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
Hello
|
||||||
|
};"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`group > supports group 2`] = `
|
||||||
|
".tw-gbesv1 {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.group\\/item:hover .tw-gbesv1 {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
.group\\/edit:hover .tw-1psr9tm {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(55 65 81 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
.group\\/edit:hover .tw-f3p2y0 {
|
||||||
|
--tw-translate-x: 0.125rem;
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(107 114 128 / var(--tw-text-opacity, 1));
|
||||||
|
}"
|
||||||
|
`;
|
@ -1,8 +1,8 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`spread > supports spread attribute in "css" attribute (2) 1`] = `
|
exports[`spread > supports spread attribute in "css" attribute (2) 1`] = `
|
||||||
"import { cx as _cx } from "@emotion/css";
|
"import * as _tslib from "tslib";
|
||||||
import * as _tslib from "tslib";
|
import { cx as _cx } from "@emotion/css";
|
||||||
import { jsx } from "react/jsx-runtime";
|
import { jsx } from "react/jsx-runtime";
|
||||||
function Hello(props) {
|
function Hello(props) {
|
||||||
props = {
|
props = {
|
||||||
|
@ -41,7 +41,7 @@ exports[`babel-tailwind > supports grouped tw 2`] = `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`babel-tailwind > supports styled components usage 1`] = `
|
exports[`babel-tailwind > supports styled components usage 1`] = `
|
||||||
"import { classed as _classed } from "@aet/tailwind/classed";
|
"import { classed as _classed } from "@aet/tailwind/utils";
|
||||||
var Div = _classed("div", "tw-gqn2k6");"
|
var Div = _classed("div", "tw-gqn2k6");"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -61,115 +61,4 @@ describe("attr", () => {
|
|||||||
|
|
||||||
matchSnapshot(files);
|
matchSnapshot(files);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("fails", async () => {
|
|
||||||
const { files } = await compileESBuild({
|
|
||||||
clsx: "emotion",
|
|
||||||
expectFiles: 2,
|
|
||||||
javascript: /* tsx */ `
|
|
||||||
|
|
||||||
type Type = "dependencies" | "devDependencies" | "peerDependencies"
|
|
||||||
|
|
||||||
const Version = classed("span", tw\`text-[var(--color-fg-muted)]\`)
|
|
||||||
|
|
||||||
function TreeItem({
|
|
||||||
name,
|
|
||||||
version,
|
|
||||||
type,
|
|
||||||
}: {
|
|
||||||
name: string
|
|
||||||
version: string
|
|
||||||
type: Type
|
|
||||||
}) {
|
|
||||||
const [open, toggle] = useToggle(false)
|
|
||||||
|
|
||||||
const query = useQuery({
|
|
||||||
...getRegistryPackageInfo(name),
|
|
||||||
enabled: open,
|
|
||||||
})
|
|
||||||
|
|
||||||
const versions = query.data ? Object.keys(query.data.versions) : undefined
|
|
||||||
const matchingVersion = versions
|
|
||||||
? semverMaxSatisfying(versions, version)
|
|
||||||
: undefined
|
|
||||||
const currentVersion =
|
|
||||||
matchingVersion != null ? query.data?.versions[matchingVersion] : undefined
|
|
||||||
const data = currentVersion
|
|
||||||
? Object.entries(currentVersion[type] ?? {})
|
|
||||||
: undefined
|
|
||||||
const isDeprecated = true
|
|
||||||
|
|
||||||
const hasNoDeps = data?.length === 0
|
|
||||||
|
|
||||||
const Icon = hasNoDeps ? SmallMinus : open ? ChevronDown : ChevronRight
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li data-loading={query.isLoading}>
|
|
||||||
<div css="flex items-center gap-1 ps-0">
|
|
||||||
<span css="leading-3">
|
|
||||||
<Icon
|
|
||||||
className={Classes.TREE_NODE_CARET}
|
|
||||||
css={["min-w-0 p-0", hasNoDeps && "cursor-default"]}
|
|
||||||
onClick={hasNoDeps ? undefined : toggle}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<Link href={\`/package/\${name}\`}>{name}</Link>
|
|
||||||
<span
|
|
||||||
css={[
|
|
||||||
"text-[var(--color-fg-muted)]",
|
|
||||||
isDeprecated && "italic line-through opacity-80",
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{version}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{!open || hasNoDeps ? null : query.error ? (
|
|
||||||
<div css="mb-2 ml-4 mt-1 max-w-96">
|
|
||||||
<Callout compact intent={Intent.DANGER} css="rounded-md">
|
|
||||||
{(query.error as Error).message}
|
|
||||||
</Callout>
|
|
||||||
</div>
|
|
||||||
) : data ? (
|
|
||||||
<ul className={Classes.LIST} css="ml-0 list-none">
|
|
||||||
{data.map(([dep, version]) => (
|
|
||||||
<TreeItem key={dep} name={dep} version={version} type={type} />
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
) : (
|
|
||||||
<div className={Classes.SKELETON} css="mb-2 ml-5 mt-1 h-4 w-40" />
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DepList({
|
|
||||||
title,
|
|
||||||
deps,
|
|
||||||
type,
|
|
||||||
}: {
|
|
||||||
title: string
|
|
||||||
deps: Record<string, string>
|
|
||||||
count?: number
|
|
||||||
type: Type
|
|
||||||
}) {
|
|
||||||
const entries = Object.entries(deps)
|
|
||||||
return (
|
|
||||||
<div className="wmde-markdown-var">
|
|
||||||
<H4>
|
|
||||||
{title} ({entries.length})
|
|
||||||
</H4>
|
|
||||||
<ul className={Classes.LIST} css="list-none p-0">
|
|
||||||
{entries.map(([dep, version]) => (
|
|
||||||
<TreeItem key={dep} name={dep} version={version} type={type} />
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
`,
|
|
||||||
});
|
|
||||||
|
|
||||||
files;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { createPostCSS } from "../index";
|
import { createPostCSS, getClassName } from "../index";
|
||||||
|
|
||||||
import { getBuild, minCSS, name } from "./utils";
|
import { getBuild, minCSS, name } from "./utils";
|
||||||
|
|
||||||
@ -10,7 +10,6 @@ describe("babel-tailwind", () => {
|
|||||||
it("supports importing tailwind/base", async () => {
|
it("supports importing tailwind/base", async () => {
|
||||||
const postcss = createPostCSS({
|
const postcss = createPostCSS({
|
||||||
tailwindConfig: {},
|
tailwindConfig: {},
|
||||||
postCSSPlugins: [],
|
|
||||||
});
|
});
|
||||||
const base = await postcss("@tailwind base;");
|
const base = await postcss("@tailwind base;");
|
||||||
const { files } = await compileESBuild({
|
const { files } = await compileESBuild({
|
||||||
@ -24,4 +23,23 @@ describe("babel-tailwind", () => {
|
|||||||
expect(files.js.text).toBe("");
|
expect(files.js.text).toBe("");
|
||||||
expect(minCSS(files.css.text)).toContain(minCSS(base));
|
expect(minCSS(files.css.text)).toContain(minCSS(base));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("supports composeRenderProps", async () => {
|
||||||
|
const { files } = await compileESBuild({
|
||||||
|
clsx: "clsx",
|
||||||
|
expectFiles: 2,
|
||||||
|
javascript: /* tsx */ `
|
||||||
|
export function Hello() {
|
||||||
|
return (
|
||||||
|
<div css="text-center">
|
||||||
|
Hello, world!
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const clsName = getClassName("text-center");
|
||||||
|
expect(files.js.text).toContain(`{ className: "${clsName}",`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -5,7 +5,7 @@ import { getBuild, matchSnapshot } from "./utils";
|
|||||||
describe("emit", () => {
|
describe("emit", () => {
|
||||||
const compileESBuild = getBuild("emit");
|
const compileESBuild = getBuild("emit");
|
||||||
|
|
||||||
it("supports emitting as CSS module", async () => {
|
it.only("supports emitting as CSS module", async () => {
|
||||||
const { files } = await compileESBuild({
|
const { files } = await compileESBuild({
|
||||||
clsx: "emotion",
|
clsx: "emotion",
|
||||||
expectFiles: 2,
|
expectFiles: 2,
|
||||||
|
26
src/__tests__/group.test.ts
Normal file
26
src/__tests__/group.test.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { describe, it } from "vitest";
|
||||||
|
|
||||||
|
import { getBuild, matchSnapshot } from "./utils";
|
||||||
|
|
||||||
|
describe("group", () => {
|
||||||
|
const compileESBuild = getBuild("group");
|
||||||
|
it("supports group", async () => {
|
||||||
|
const { files } = await compileESBuild({
|
||||||
|
clsx: "emotion",
|
||||||
|
// expectFiles: 2,
|
||||||
|
javascript: /* tsx */ `
|
||||||
|
export function Hello() {
|
||||||
|
return (
|
||||||
|
<li css="group/item">
|
||||||
|
<a css="group/edit invisible group-hover/item:visible" href="tel:{person.phone}">
|
||||||
|
<span css="group-hover/edit:text-gray-700">Call</span>
|
||||||
|
<svg css="group-hover/edit:translate-x-0.5 group-hover/edit:text-gray-500"></svg>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
matchSnapshot(files);
|
||||||
|
});
|
||||||
|
});
|
@ -50,6 +50,50 @@ describe("merges with existing className attribute", () => {
|
|||||||
expect(files.css.text).toMatch(`.${clsName} {\n text-align: center;\n}`);
|
expect(files.css.text).toMatch(`.${clsName} {\n text-align: center;\n}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("supports composeRenderProps", async () => {
|
||||||
|
const { files } = await compileESBuild({
|
||||||
|
clsx: "clsx",
|
||||||
|
expectFiles: 2,
|
||||||
|
composeRenderProps: true,
|
||||||
|
javascript: /* tsx */ `
|
||||||
|
export function Hello(props) {
|
||||||
|
return (
|
||||||
|
<div {...props} css="text-center">
|
||||||
|
Hello, world!
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const clsName = getClassName("text-center");
|
||||||
|
expect(files.js.text).toContain(
|
||||||
|
`{ ...props, className: _composeClassName("${clsName}", _className),`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports composeRenderProps (2)", async () => {
|
||||||
|
const { files } = await compileESBuild({
|
||||||
|
clsx: "clsx",
|
||||||
|
expectFiles: 2,
|
||||||
|
composeRenderProps: true,
|
||||||
|
javascript: /* tsx */ `
|
||||||
|
export function Hello({ className, ...props }) {
|
||||||
|
return (
|
||||||
|
<div className={className} css="text-center">
|
||||||
|
<span {...props}>Hello, world!</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const clsName = getClassName("text-center");
|
||||||
|
expect(files.js.text).toContain(
|
||||||
|
`{ className: _composeClassName("${clsName}", className),`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("reuses clsx in scope", async () => {
|
it("reuses clsx in scope", async () => {
|
||||||
const { files } = await compileESBuild({
|
const { files } = await compileESBuild({
|
||||||
clsx: "clsx",
|
clsx: "clsx",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { promises as fs } from "node:fs";
|
import { promises as fs } from "node:fs";
|
||||||
import { join, resolve } from "node:path";
|
import { join, resolve } from "node:path";
|
||||||
|
|
||||||
|
import type { PluginItem } from "@babel/core";
|
||||||
import dedent from "dedent";
|
import dedent from "dedent";
|
||||||
import * as esbuild from "esbuild";
|
import * as esbuild from "esbuild";
|
||||||
import { afterEach, beforeEach, expect } from "vitest";
|
import { afterEach, beforeEach, expect } from "vitest";
|
||||||
@ -35,12 +36,14 @@ export function getBuild(name: string) {
|
|||||||
return async function compileESBuild({
|
return async function compileESBuild({
|
||||||
javascript,
|
javascript,
|
||||||
esbuild: esbuildOptions,
|
esbuild: esbuildOptions,
|
||||||
|
babelPlugins,
|
||||||
expectFiles,
|
expectFiles,
|
||||||
...options
|
...options
|
||||||
}: Omit<TailwindPluginOptions, "compile"> & {
|
}: Omit<TailwindPluginOptions, "compile"> & {
|
||||||
esbuild?: esbuild.BuildOptions;
|
esbuild?: esbuild.BuildOptions;
|
||||||
javascript: string;
|
javascript: string;
|
||||||
expectFiles?: number;
|
expectFiles?: number;
|
||||||
|
babelPlugins?: PluginItem[];
|
||||||
}) {
|
}) {
|
||||||
const tailwind = getTailwindPlugins({
|
const tailwind = getTailwindPlugins({
|
||||||
tailwindConfig: {},
|
tailwindConfig: {},
|
||||||
@ -52,15 +55,19 @@ export function getBuild(name: string) {
|
|||||||
external: [
|
external: [
|
||||||
"react",
|
"react",
|
||||||
"react/jsx-runtime",
|
"react/jsx-runtime",
|
||||||
|
"react-aria-components",
|
||||||
"@emotion/css",
|
"@emotion/css",
|
||||||
"clsx",
|
"clsx",
|
||||||
"tslib",
|
"tslib",
|
||||||
"@aet/tailwind/classed",
|
"@aet/tailwind/utils",
|
||||||
],
|
],
|
||||||
outdir: "dist",
|
outdir: "dist",
|
||||||
format: "esm",
|
format: "esm",
|
||||||
entryPoints: [await write("index.tsx", dedent(javascript))],
|
entryPoints: [await write("index.tsx", dedent(javascript))],
|
||||||
plugins: [babelPlugin({ plugins: [tailwind.babel()] }), tailwind.esbuild()],
|
plugins: [
|
||||||
|
babelPlugin({ plugins: [tailwind.babel(), ...(babelPlugins ?? [])] }),
|
||||||
|
tailwind.esbuild(),
|
||||||
|
],
|
||||||
...esbuildOptions,
|
...esbuildOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import { basename, dirname, extname, join } from "node:path";
|
import { basename, dirname, extname, join } from "node:path";
|
||||||
|
|
||||||
import type b from "@babel/core";
|
import type b from "@babel/core";
|
||||||
import { type NodePath, type types as t } from "@babel/core";
|
import { type Node, type NodePath, type types as t } from "@babel/core";
|
||||||
import hash from "@emotion/hash";
|
import hash from "@emotion/hash";
|
||||||
|
import { memoize } from "lodash-es";
|
||||||
import invariant from "tiny-invariant";
|
import invariant from "tiny-invariant";
|
||||||
|
|
||||||
import { type ResolveTailwindOptions, getClassName } from "../index";
|
import { type ResolveTailwindOptions, getClassName } from "../index";
|
||||||
import { type SourceLocation, type StyleMapEntry, classedName } from "../shared";
|
import { type SourceLocation, type StyleMapEntry, utilsName } from "../shared";
|
||||||
|
|
||||||
import { handleMacro } from "./macro";
|
import { handleMacro } from "./macro";
|
||||||
import { evaluateArgs, trim } from "./utils";
|
import { evaluateArgs, trim } from "./utils";
|
||||||
@ -49,10 +50,6 @@ function getUtils({
|
|||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
let cx: t.Identifier;
|
let cx: t.Identifier;
|
||||||
let tslibImport: t.Identifier;
|
|
||||||
let styleImport: t.Identifier;
|
|
||||||
let classedImport: t.Identifier;
|
|
||||||
let cssModuleImport: t.Identifier;
|
|
||||||
|
|
||||||
const cssMap = new Map<string, StyleMapEntry>();
|
const cssMap = new Map<string, StyleMapEntry>();
|
||||||
const jsMap = new Map<string, StyleMapEntry>();
|
const jsMap = new Map<string, StyleMapEntry>();
|
||||||
@ -87,23 +84,40 @@ function getUtils({
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStyleImport() {
|
function reuseImport(scope: Scope) {
|
||||||
styleImport ??= path.scope.generateUidIdentifier("styles");
|
if (
|
||||||
return t.cloneNode(styleImport);
|
existingCx &&
|
||||||
|
scope.getBinding(existingCx) === path.scope.getBinding(existingCx)
|
||||||
|
) {
|
||||||
|
return t.identifier(existingCx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCssModuleImport = () => {
|
function cacheNode<N extends Node>(fn: () => N) {
|
||||||
if (cssModuleImport == null) {
|
let cache: N | undefined;
|
||||||
cssModuleImport = path.scope.generateUidIdentifier("cssModule");
|
return Object.assign(
|
||||||
}
|
(): N => {
|
||||||
return t.cloneNode(cssModuleImport);
|
cache ??= fn();
|
||||||
};
|
return t.cloneNode(cache);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
getCache() {
|
||||||
|
return cache;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const reuseImport = (scope: Scope, id?: string) => {
|
const getStyleImport = cacheNode(() => path.scope.generateUidIdentifier("styles"));
|
||||||
if (id && scope.getBinding(id) === path.scope.getBinding(id)) {
|
const getCssModuleImport = cacheNode(() =>
|
||||||
return t.identifier(id);
|
path.scope.generateUidIdentifier("cssModule")
|
||||||
}
|
);
|
||||||
};
|
|
||||||
|
const getUtilsImport = memoize(() => {
|
||||||
|
const importDecl = t.importDeclaration([], t.stringLiteral(utilsName));
|
||||||
|
path.node.body.unshift(importDecl);
|
||||||
|
return importDecl;
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
program: path,
|
program: path,
|
||||||
@ -151,40 +165,43 @@ function getUtils({
|
|||||||
|
|
||||||
getCx: (localScope: Scope) => {
|
getCx: (localScope: Scope) => {
|
||||||
if (cx == null) {
|
if (cx == null) {
|
||||||
const reuse = reuseImport(localScope, existingCx);
|
const reuse = reuseImport(localScope);
|
||||||
if (reuse) return reuse;
|
if (reuse) return reuse;
|
||||||
|
|
||||||
cx = path.scope.generateUidIdentifier("cx");
|
cx = path.scope.generateUidIdentifier("cx");
|
||||||
path.node.body.unshift(getClsxImport(t, cx, clsx));
|
// If you unshift, react-refresh/babel will insert _s(Component) right above
|
||||||
|
// the component declaration, which is invalid.
|
||||||
|
path.node.body.push(getClsxImport(t, cx, clsx));
|
||||||
}
|
}
|
||||||
return t.cloneNode(cx);
|
return t.cloneNode(cx);
|
||||||
},
|
},
|
||||||
|
|
||||||
getTSlibImport: () => {
|
getTSlibImport: cacheNode(() => {
|
||||||
if (tslibImport == null) {
|
const tslibImport = path.scope.generateUidIdentifier("tslib");
|
||||||
tslibImport = path.scope.generateUidIdentifier("tslib");
|
path.node.body.push(
|
||||||
path.node.body.unshift(
|
t.importDeclaration(
|
||||||
t.importDeclaration(
|
[t.importNamespaceSpecifier(tslibImport)],
|
||||||
[t.importNamespaceSpecifier(tslibImport)],
|
t.stringLiteral("tslib")
|
||||||
t.stringLiteral("tslib")
|
)
|
||||||
)
|
);
|
||||||
);
|
return tslibImport;
|
||||||
}
|
}),
|
||||||
return t.cloneNode(tslibImport);
|
|
||||||
},
|
|
||||||
|
|
||||||
getClassedImport: () => {
|
getClsCompose: cacheNode(() => {
|
||||||
if (classedImport == null) {
|
const clsComposeImport = path.scope.generateUidIdentifier("composeClassName");
|
||||||
classedImport = path.scope.generateUidIdentifier("classed");
|
getUtilsImport().specifiers.push(
|
||||||
path.node.body.unshift(
|
t.importSpecifier(clsComposeImport, t.identifier("composeClassName"))
|
||||||
t.importDeclaration(
|
);
|
||||||
[t.importSpecifier(classedImport, t.identifier("classed"))],
|
return clsComposeImport;
|
||||||
t.stringLiteral(classedName)
|
}),
|
||||||
)
|
|
||||||
);
|
getClassedImport: cacheNode(() => {
|
||||||
}
|
const classedImport = path.scope.generateUidIdentifier("classed");
|
||||||
return t.cloneNode(classedImport);
|
getUtilsImport().specifiers.push(
|
||||||
},
|
t.importSpecifier(classedImport, t.identifier("classed"))
|
||||||
|
);
|
||||||
|
return classedImport;
|
||||||
|
}),
|
||||||
|
|
||||||
getCssModuleImport,
|
getCssModuleImport,
|
||||||
|
|
||||||
@ -204,6 +221,7 @@ function getUtils({
|
|||||||
if (!cssMap.size && !jsMap.size) return;
|
if (!cssMap.size && !jsMap.size) return;
|
||||||
invariant(filename, "babel: missing state.filename");
|
invariant(filename, "babel: missing state.filename");
|
||||||
|
|
||||||
|
const cssModuleImport = getCssModuleImport.getCache();
|
||||||
if (cssMap.size) {
|
if (cssMap.size) {
|
||||||
const cssName =
|
const cssName =
|
||||||
basename(filename, extname(filename)) +
|
basename(filename, extname(filename)) +
|
||||||
@ -256,6 +274,7 @@ export function babelTailwind(
|
|||||||
getClassName: getClass = getClassName,
|
getClassName: getClass = getClassName,
|
||||||
jsxAttributeAction = "delete",
|
jsxAttributeAction = "delete",
|
||||||
jsxAttributeName = "css",
|
jsxAttributeName = "css",
|
||||||
|
composeRenderProps,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
return definePlugin<BabelPluginUtils>(({ types: t }) => ({
|
return definePlugin<BabelPluginUtils>(({ types: t }) => ({
|
||||||
@ -334,6 +353,13 @@ export function babelTailwind(
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const {
|
||||||
|
identifier: id,
|
||||||
|
jsxExpressionContainer: jsxBox,
|
||||||
|
jsxIdentifier: jsxId,
|
||||||
|
callExpression: call,
|
||||||
|
} = t;
|
||||||
|
|
||||||
let valuePathNode = extractJSXContainer(valuePath.node);
|
let valuePathNode = extractJSXContainer(valuePath.node);
|
||||||
if (
|
if (
|
||||||
t.isArrayExpression(valuePathNode) &&
|
t.isArrayExpression(valuePathNode) &&
|
||||||
@ -345,101 +371,105 @@ export function babelTailwind(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const wrap = (existing: b.types.Expression) =>
|
||||||
|
composeRenderProps
|
||||||
|
? call(_.getClsCompose(), [valuePathNode, existing])
|
||||||
|
: call(_.getCx(path.scope), [valuePathNode, existing]);
|
||||||
|
|
||||||
|
// There is an existing className attribute
|
||||||
if (classNameAttribute) {
|
if (classNameAttribute) {
|
||||||
const attrValue = classNameAttribute.value!;
|
const attrValue = classNameAttribute.value!;
|
||||||
const wrap = (...originalValue: (b.types.Expression | b.types.SpreadElement)[]) =>
|
|
||||||
t.callExpression(_.getCx(path.scope), [valuePathNode, ...originalValue]);
|
|
||||||
|
|
||||||
// If both are string literals, we can merge them directly here
|
// If both are string literals, we can merge them directly here
|
||||||
if (t.isStringLiteral(attrValue) && t.isStringLiteral(valuePathNode)) {
|
if (t.isStringLiteral(attrValue) && t.isStringLiteral(valuePathNode)) {
|
||||||
attrValue.value +=
|
attrValue.value +=
|
||||||
(attrValue.value.at(-1) === " " ? "" : " ") + valuePathNode.value;
|
(attrValue.value.at(-1) === " " ? "" : " ") + valuePathNode.value;
|
||||||
} else {
|
} else {
|
||||||
const internalAttrValue = extractJSXContainer(attrValue);
|
const internal = extractJSXContainer(attrValue);
|
||||||
if (
|
if (
|
||||||
t.isArrowFunctionExpression(internalAttrValue) &&
|
t.isArrowFunctionExpression(internal) &&
|
||||||
!t.isBlockStatement(internalAttrValue.body)
|
!t.isBlockStatement(internal.body)
|
||||||
) {
|
) {
|
||||||
internalAttrValue.body = wrap(internalAttrValue.body);
|
// className={({ isEntering }) => isEntering ? "enter" : "exit"}
|
||||||
|
// className: ({ isEntering }) => _cx("${clsName}", isEntering ? "enter" : "exit")
|
||||||
|
internal.body = wrap(internal.body);
|
||||||
} else if (
|
} else if (
|
||||||
// if the existing className is already wrapped with cx, we unwrap it
|
// if the existing className is already wrapped with cx, we unwrap it
|
||||||
// to avoid double calling: cx(cx())
|
// to avoid double calling: cx(cx())
|
||||||
t.isCallExpression(internalAttrValue) &&
|
t.isCallExpression(internal) &&
|
||||||
t.isIdentifier(internalAttrValue.callee) &&
|
t.isIdentifier(internal.callee) &&
|
||||||
_.existingCx &&
|
_.existingCx &&
|
||||||
_.program.scope
|
_.program.scope
|
||||||
.getBinding(_.existingCx)
|
.getBinding(_.existingCx)
|
||||||
?.referencePaths.map(p => p.node)
|
?.referencePaths.map(p => p.node)
|
||||||
.includes(internalAttrValue.callee)
|
.includes(internal.callee)
|
||||||
) {
|
) {
|
||||||
classNameAttribute.value = t.jsxExpressionContainer(
|
classNameAttribute.value = jsxBox(
|
||||||
wrap(
|
call(_.getCx(path.scope), [
|
||||||
...(internalAttrValue.arguments as (
|
valuePathNode,
|
||||||
| b.types.Expression
|
...(internal.arguments as (b.types.Expression | b.types.SpreadElement)[]),
|
||||||
| b.types.SpreadElement
|
])
|
||||||
)[])
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
classNameAttribute.value = t.jsxExpressionContainer(wrap(internalAttrValue));
|
classNameAttribute.value = jsxBox(wrap(internal));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const wrap = (originalValue: b.types.Expression) =>
|
|
||||||
t.callExpression(_.getCx(path.scope), [valuePathNode, originalValue]);
|
|
||||||
|
|
||||||
const rest = parent.attributes.filter(attr => t.isJSXSpreadAttribute(attr));
|
const rest = parent.attributes.filter(attr => t.isJSXSpreadAttribute(attr));
|
||||||
let arg;
|
let arg;
|
||||||
|
// if there is only one JSX spread attribute and it's an identifier
|
||||||
|
// ... {...props} />
|
||||||
if (rest.length === 1 && (arg = rest[0].argument) && t.isIdentifier(arg)) {
|
if (rest.length === 1 && (arg = rest[0].argument) && t.isIdentifier(arg)) {
|
||||||
// props from argument and not modified anywhere
|
// props from argument and not modified anywhere, get the declaration of this argument
|
||||||
const scope = path.scope.getBinding(arg.name);
|
const scope = path.scope.getBinding(arg.name);
|
||||||
let index: number;
|
let index: number;
|
||||||
|
// node is an identifier or object pattern in `params`
|
||||||
|
// (props) => ... or ({ ...props }) => ...
|
||||||
const node = scope?.path.node;
|
const node = scope?.path.node;
|
||||||
if (
|
if (
|
||||||
scope &&
|
scope &&
|
||||||
!scope.constantViolations.length &&
|
!scope.constantViolations.length &&
|
||||||
t.isFunctionDeclaration(scope.path.parent) &&
|
(t.isFunctionDeclaration(scope.path.parent) ||
|
||||||
|
t.isArrowFunctionExpression(scope.path.parent)) &&
|
||||||
(index = (scope.path.parent.params as t.Node[]).indexOf(node!)) !== -1 &&
|
(index = (scope.path.parent.params as t.Node[]).indexOf(node!)) !== -1 &&
|
||||||
(t.isIdentifier(node) || t.isObjectPattern(node))
|
(t.isIdentifier(node) || t.isObjectPattern(node))
|
||||||
) {
|
) {
|
||||||
const clsVar = path.scope.generateUidIdentifier("className");
|
const clsVar = path.scope.generateUidIdentifier("className");
|
||||||
if (t.isIdentifier(node)) {
|
if (t.isIdentifier(node)) {
|
||||||
|
// (props) => ...
|
||||||
|
// ↪ ({ className, ...props }) => ...
|
||||||
scope.path.parent.params[index] = t.objectPattern([
|
scope.path.parent.params[index] = t.objectPattern([
|
||||||
t.objectProperty(t.identifier("className"), clsVar),
|
t.objectProperty(id("className"), clsVar),
|
||||||
t.restElement(node),
|
t.restElement(node),
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
node.properties.unshift(
|
// ({ ...props }) => ...
|
||||||
t.objectProperty(t.identifier("className"), clsVar)
|
// ↪ ({ className, ...props }) => ...
|
||||||
);
|
node.properties.unshift(t.objectProperty(id("className"), clsVar));
|
||||||
}
|
}
|
||||||
|
|
||||||
parent.attributes.push(
|
parent.attributes.push(
|
||||||
t.jsxAttribute(
|
t.jsxAttribute(jsxId("className"), jsxBox(wrap(clsVar)))
|
||||||
t.jsxIdentifier("className"),
|
|
||||||
t.jsxExpressionContainer(wrap(clsVar))
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const tslibImport = _.getTSlibImport();
|
const tslibImport = _.getTSlibImport();
|
||||||
rest[0].argument = t.callExpression(
|
rest[0].argument = call(t.memberExpression(tslibImport, id("__rest")), [
|
||||||
t.memberExpression(tslibImport, t.identifier("__rest")),
|
arg,
|
||||||
[arg, t.arrayExpression([t.stringLiteral("className")])]
|
t.arrayExpression([t.stringLiteral("className")]),
|
||||||
);
|
]);
|
||||||
|
|
||||||
parent.attributes.push(
|
parent.attributes.push(
|
||||||
t.jsxAttribute(
|
t.jsxAttribute(
|
||||||
t.jsxIdentifier("className"),
|
jsxId("className"),
|
||||||
t.jsxExpressionContainer(
|
jsxBox(wrap(t.memberExpression(arg, id("className"))))
|
||||||
wrap(t.memberExpression(arg, t.identifier("className")))
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Fallback
|
||||||
const containerValue = t.isStringLiteral(valuePathNode)
|
const containerValue = t.isStringLiteral(valuePathNode)
|
||||||
? valuePathNode
|
? valuePathNode
|
||||||
: t.callExpression(_.getCx(path.scope), [valuePathNode]);
|
: call(_.getCx(path.scope), [valuePathNode]);
|
||||||
|
|
||||||
parent.attributes.push(
|
parent.attributes.push(
|
||||||
t.jsxAttribute(
|
t.jsxAttribute(
|
||||||
|
@ -59,7 +59,7 @@ export function handleMacro({
|
|||||||
_.replaceWithImport({
|
_.replaceWithImport({
|
||||||
type,
|
type,
|
||||||
path: callee,
|
path: callee,
|
||||||
className: addIf(className, list.includes("group") && " group"),
|
className: addIf(className, addGroup(list)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,12 +77,17 @@ export function handleMacro({
|
|||||||
_.replaceWithImport({
|
_.replaceWithImport({
|
||||||
type,
|
type,
|
||||||
path: callee,
|
path: callee,
|
||||||
className: addIf(className, list.includes("group") && " group"),
|
className: addIf(className, addGroup(list)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addGroup(list: string[]) {
|
||||||
|
const groups = list.filter(name => name === "group" || name.startsWith("group/"));
|
||||||
|
return groups.length ? ` ${groups.join(" ")}` : false;
|
||||||
|
}
|
||||||
|
|
||||||
function addIf(text: string, suffix: string | false) {
|
function addIf(text: string, suffix: string | false) {
|
||||||
return suffix ? text + suffix : text;
|
return suffix ? text + suffix : text;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
import type { NodePath } from "@babel/core";
|
||||||
import { isPlainObject } from "lodash-es";
|
import { isPlainObject } from "lodash-es";
|
||||||
import invariant from "tiny-invariant";
|
import invariant from "tiny-invariant";
|
||||||
import { type NodePath } from "@babel/core";
|
|
||||||
|
|
||||||
export function evaluateArgs(path: NodePath) {
|
export function evaluateArgs(path: NodePath) {
|
||||||
const { confident, value } = path.evaluate();
|
const { confident, value } = path.evaluate();
|
||||||
|
79
src/index.ts
79
src/index.ts
@ -1,9 +1,12 @@
|
|||||||
|
import { createRequire } from "node:module";
|
||||||
|
|
||||||
import hash from "@emotion/hash";
|
import hash from "@emotion/hash";
|
||||||
|
import type { BabelOptions, Options as ReactOptions } from "@vitejs/plugin-react";
|
||||||
import { transformSync } from "esbuild";
|
import { transformSync } from "esbuild";
|
||||||
import { memoize, without } from "lodash-es";
|
import { memoize, without } from "lodash-es";
|
||||||
import type postcss from "postcss";
|
|
||||||
import type { Config } from "tailwindcss";
|
import type { Config } from "tailwindcss";
|
||||||
import type { SetRequired } from "type-fest";
|
import type { SetRequired } from "type-fest";
|
||||||
|
import type { PluginOption } from "vite";
|
||||||
|
|
||||||
import { type ClassNameCollector, babelTailwind } from "./babel/index";
|
import { type ClassNameCollector, babelTailwind } from "./babel/index";
|
||||||
import { toJSCode } from "./css-to-js";
|
import { toJSCode } from "./css-to-js";
|
||||||
@ -21,6 +24,8 @@ export type BuildStyleFile = (
|
|||||||
path: string
|
path: string
|
||||||
) => Promise<readonly ["css" | "local-css", string] | readonly ["js", string]>;
|
) => Promise<readonly ["css" | "local-css", string] | readonly ["js", string]>;
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
export interface TailwindPluginOptions {
|
export interface TailwindPluginOptions {
|
||||||
/**
|
/**
|
||||||
* Tailwind CSS configuration
|
* Tailwind CSS configuration
|
||||||
@ -37,11 +42,6 @@ export interface TailwindPluginOptions {
|
|||||||
*/
|
*/
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Additional PostCSS plugins (optional)
|
|
||||||
*/
|
|
||||||
postCSSPlugins?: postcss.AcceptedPlugin[];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attribute to use for tailwind classes in JSX
|
* Attribute to use for tailwind classes in JSX
|
||||||
* @default "css"
|
* @default "css"
|
||||||
@ -55,7 +55,7 @@ export interface TailwindPluginOptions {
|
|||||||
jsxAttributeAction?: "delete" | "preserve" | ["rename", string];
|
jsxAttributeAction?: "delete" | "preserve" | ["rename", string];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The prefix to use for the generated class names.
|
* The function to use to generate class names.
|
||||||
* @default className => `tw-${hash(className)}`
|
* @default className => `tw-${hash(className)}`
|
||||||
*/
|
*/
|
||||||
getClassName?: GetClassName;
|
getClassName?: GetClassName;
|
||||||
@ -65,6 +65,11 @@ export interface TailwindPluginOptions {
|
|||||||
*/
|
*/
|
||||||
clsx: "clsx" | "classnames" | "emotion";
|
clsx: "clsx" | "classnames" | "emotion";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use react-aria-component’s `composeRenderProps` function.
|
||||||
|
*/
|
||||||
|
composeRenderProps?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@ -101,12 +106,7 @@ export interface TailwindPluginOptions {
|
|||||||
|
|
||||||
export type ResolveTailwindOptions = SetRequired<
|
export type ResolveTailwindOptions = SetRequired<
|
||||||
TailwindPluginOptions,
|
TailwindPluginOptions,
|
||||||
| "clsx"
|
"clsx" | "jsxAttributeAction" | "jsxAttributeName" | "styleMap" | "tailwindConfig"
|
||||||
| "jsxAttributeAction"
|
|
||||||
| "jsxAttributeName"
|
|
||||||
| "postCSSPlugins"
|
|
||||||
| "styleMap"
|
|
||||||
| "tailwindConfig"
|
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -115,6 +115,15 @@ export type ResolveTailwindOptions = SetRequired<
|
|||||||
*/
|
*/
|
||||||
export const getClassName: GetClassName = cls => "tw-" + hash(cls);
|
export const getClassName: GetClassName = cls => "tw-" + hash(cls);
|
||||||
|
|
||||||
|
type BabelPlugin =
|
||||||
|
| "@emotion/babel-plugin"
|
||||||
|
| "@lingui/babel-plugin-lingui-macro"
|
||||||
|
| "babel-plugin-import"
|
||||||
|
| "babel-plugin-react-compiler"
|
||||||
|
| "jotai/babel/plugin-debug-label"
|
||||||
|
| "jotai/babel/plugin-react-refresh"
|
||||||
|
| (string & {});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main entry. Returns the plugins and utilities for processing Tailwind
|
* Main entry. Returns the plugins and utilities for processing Tailwind
|
||||||
* classNames in JS.
|
* classNames in JS.
|
||||||
@ -137,7 +146,6 @@ export function getTailwindPlugins(options: TailwindPluginOptions) {
|
|||||||
getClassName,
|
getClassName,
|
||||||
jsxAttributeAction: "delete",
|
jsxAttributeAction: "delete",
|
||||||
jsxAttributeName: "css",
|
jsxAttributeName: "css",
|
||||||
postCSSPlugins: [],
|
|
||||||
styleMap: new Map(),
|
styleMap: new Map(),
|
||||||
tailwindConfig: {},
|
tailwindConfig: {},
|
||||||
...options,
|
...options,
|
||||||
@ -153,7 +161,12 @@ export function getTailwindPlugins(options: TailwindPluginOptions) {
|
|||||||
const compiled = await compile(
|
const compiled = await compile(
|
||||||
styles
|
styles
|
||||||
.map(({ classNames, key }) => {
|
.map(({ classNames, key }) => {
|
||||||
const tw = without(classNames, "group").join(" ");
|
const tw = without(classNames, "group", "peer")
|
||||||
|
.filter(name => !name.startsWith("group/"))
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
if (!tw) return "";
|
||||||
|
|
||||||
return [
|
return [
|
||||||
`.${key} {`,
|
`.${key} {`,
|
||||||
addSourceAsComment && ` /* @preserve ${tw} */`,
|
addSourceAsComment && ` /* @preserve ${tw} */`,
|
||||||
@ -178,15 +191,49 @@ export function getTailwindPlugins(options: TailwindPluginOptions) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const babel = (onCollect?: ClassNameCollector) =>
|
||||||
|
babelTailwind(resolvedOptions, onCollect);
|
||||||
|
|
||||||
|
const patchBabelOptions = (options: BabelOptions) => {
|
||||||
|
(options.plugins ??= []).push(babel());
|
||||||
|
};
|
||||||
|
|
||||||
|
const react: {
|
||||||
|
(babelPlugins: (BabelPlugin | BabelPlugin[] | false)[]): PluginOption[];
|
||||||
|
(options?: ReactOptions): PluginOption[];
|
||||||
|
} = (options: (string | string[] | false)[] | ReactOptions = {}) => {
|
||||||
|
options = Array.isArray(options)
|
||||||
|
? ({ babel: { plugins: options.flat(1).filter(Boolean) } } satisfies ReactOptions)
|
||||||
|
: options;
|
||||||
|
|
||||||
|
const reactModule = require("@vitejs/plugin-react");
|
||||||
|
const reactPlugin: typeof import("@vitejs/plugin-react").default =
|
||||||
|
"default" in reactModule ? reactModule.default : reactModule;
|
||||||
|
|
||||||
|
options.babel ??= {};
|
||||||
|
if (typeof options.babel === "function") {
|
||||||
|
const fn = options.babel;
|
||||||
|
options.babel = (id, options) => {
|
||||||
|
const result = fn(id, options);
|
||||||
|
patchBabelOptions(result);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
patchBabelOptions(options.babel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reactPlugin(options);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
compile,
|
compile,
|
||||||
babel: (onCollect?: ClassNameCollector) => babelTailwind(resolvedOptions, onCollect),
|
babel: (onCollect?: ClassNameCollector) => babelTailwind(resolvedOptions, onCollect),
|
||||||
esbuild: () => esbuildPlugin({ styleMap, compile, buildStyleFile }),
|
esbuild: () => esbuildPlugin({ styleMap, compile, buildStyleFile }),
|
||||||
/** Requires `options.vite` to be `true`. */
|
|
||||||
vite: () => {
|
vite: () => {
|
||||||
resolvedOptions.vite = true;
|
resolvedOptions.vite = true;
|
||||||
return vitePlugin({ styleMap, compile, buildStyleFile });
|
return vitePlugin({ styleMap, compile, buildStyleFile });
|
||||||
},
|
},
|
||||||
|
react,
|
||||||
styleMap,
|
styleMap,
|
||||||
options,
|
options,
|
||||||
getCompiler,
|
getCompiler,
|
||||||
|
5
src/react-env.d.ts
vendored
Normal file
5
src/react-env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
declare namespace React {
|
||||||
|
interface Attributes {
|
||||||
|
css?: string;
|
||||||
|
}
|
||||||
|
}
|
55
src/shared-v4.ts
Normal file
55
src/shared-v4.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import * as tailwind from "tailwindcss";
|
||||||
|
|
||||||
|
import type { ResolveTailwindOptions } from "./index";
|
||||||
|
|
||||||
|
declare const __PKG_NAME__: string;
|
||||||
|
export const pkgName = __PKG_NAME__;
|
||||||
|
|
||||||
|
export const macroNames = [`${pkgName}/macro`, `${pkgName}/µ`];
|
||||||
|
export const utilsName = `${pkgName}/utils`;
|
||||||
|
|
||||||
|
interface LineColumn {
|
||||||
|
line: number;
|
||||||
|
column: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SourceLocation {
|
||||||
|
filename: string;
|
||||||
|
start: LineColumn;
|
||||||
|
end: LineColumn;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StyleMapEntry {
|
||||||
|
key: string;
|
||||||
|
classNames: string[];
|
||||||
|
location: SourceLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const tailwindDirectives = ["components", "utilities", "variants"] as const;
|
||||||
|
|
||||||
|
export type StyleMap = Map</* filename */ string, StyleMapEntry[]>;
|
||||||
|
|
||||||
|
export function createPostCSS({
|
||||||
|
tailwindConfig,
|
||||||
|
prefix,
|
||||||
|
}: Pick<ResolveTailwindOptions, "tailwindConfig" | "prefix">) {
|
||||||
|
return async (css: string) => {
|
||||||
|
const compiler = await tailwind.compile(
|
||||||
|
[prefix, '@config "tailwind.config.js";', css].filter(Boolean).join("\n"),
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line @typescript-eslint/require-await
|
||||||
|
async loadModule(_id, base, resourceHint) {
|
||||||
|
if (resourceHint === "config") {
|
||||||
|
return { base, module: tailwindConfig };
|
||||||
|
}
|
||||||
|
throw new Error(`The browser build does not support plugins or config files.`);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return compiler.build([]).replace(/^\/\*![^*]*\*\/\n/, "");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Compile = ReturnType<typeof createPostCSS>;
|
@ -7,7 +7,7 @@ declare const __PKG_NAME__: string;
|
|||||||
export const pkgName = __PKG_NAME__;
|
export const pkgName = __PKG_NAME__;
|
||||||
|
|
||||||
export const macroNames = [`${pkgName}/macro`, `${pkgName}/µ`];
|
export const macroNames = [`${pkgName}/macro`, `${pkgName}/µ`];
|
||||||
export const classedName = `${pkgName}/classed`;
|
export const utilsName = `${pkgName}/utils`;
|
||||||
|
|
||||||
interface LineColumn {
|
interface LineColumn {
|
||||||
line: number;
|
line: number;
|
||||||
@ -33,15 +33,13 @@ export type StyleMap = Map</* filename */ string, StyleMapEntry[]>;
|
|||||||
|
|
||||||
export function createPostCSS({
|
export function createPostCSS({
|
||||||
tailwindConfig,
|
tailwindConfig,
|
||||||
postCSSPlugins,
|
|
||||||
prefix,
|
prefix,
|
||||||
}: Pick<ResolveTailwindOptions, "tailwindConfig" | "postCSSPlugins" | "prefix">) {
|
}: Pick<ResolveTailwindOptions, "tailwindConfig" | "prefix">) {
|
||||||
const post = postcss([
|
const post = postcss([
|
||||||
tailwind({
|
tailwind({
|
||||||
...tailwindConfig,
|
...tailwindConfig,
|
||||||
content: [{ raw: "<br>", extension: "html" }],
|
content: [{ raw: "<br>", extension: "html" }],
|
||||||
}),
|
}),
|
||||||
...postCSSPlugins,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return async (css: string) => {
|
return async (css: string) => {
|
||||||
|
@ -67,3 +67,10 @@ export const classed: {
|
|||||||
component.className = className;
|
component.className = className;
|
||||||
return component;
|
return component;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function composeClassName<T>(
|
||||||
|
value: string,
|
||||||
|
prop: string | ((values: T) => string) | undefined
|
||||||
|
) {
|
||||||
|
return typeof prop === "function" ? (arg: T) => cx(value, prop(arg)) : cx(value, prop);
|
||||||
|
}
|
19
src/vendor/v4.ts
vendored
Normal file
19
src/vendor/v4.ts
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import plugin from "tailwindcss/plugin.js";
|
||||||
|
import type { PluginAPI } from "tailwindcss/types/config";
|
||||||
|
|
||||||
|
export default plugin(({ addUtilities, matchUtilities, theme }) => {
|
||||||
|
matchUtilities(
|
||||||
|
{
|
||||||
|
ms: value => ({
|
||||||
|
marginInlineStart: value,
|
||||||
|
}),
|
||||||
|
me: value => ({
|
||||||
|
marginInlineEnd: value,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
supportsNegativeValues: true,
|
||||||
|
values: theme("margin")!,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
@ -1,6 +1,9 @@
|
|||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
|
|
||||||
import type * as vite from "vite";
|
import type * as vite from "vite";
|
||||||
|
|
||||||
import { type Compile, type StyleMap, macroNames, pkgName } from "./shared";
|
import { type Compile, type StyleMap, macroNames, pkgName } from "./shared";
|
||||||
|
|
||||||
import type { BuildStyleFile } from "./index";
|
import type { BuildStyleFile } from "./index";
|
||||||
|
|
||||||
const ROLLUP_PREFIX = "\0tailwind:";
|
const ROLLUP_PREFIX = "\0tailwind:";
|
||||||
@ -59,4 +62,4 @@ export const vitePlugin = ({
|
|||||||
* `babel-plugin-macros` compatible `isMacrosName` function that works with this plugin.
|
* `babel-plugin-macros` compatible `isMacrosName` function that works with this plugin.
|
||||||
*/
|
*/
|
||||||
export const isMacrosName = (v: string) =>
|
export const isMacrosName = (v: string) =>
|
||||||
!macroNames.includes(v) && /[./]macro(\.c?js)?$/.test(v);
|
!macroNames.includes(v) && /[./]macro(?:\.c?js)?$/.test(v);
|
||||||
|
Reference in New Issue
Block a user