Compare commits
10 Commits
f741ba41c2
...
main
Author | SHA1 | Date | |
---|---|---|---|
1d0a8a1c36 | |||
bd683df539 | |||
52b19b3b36 | |||
8e41208a14 | |||
17f9527ab4 | |||
08cd7940e2 | |||
1f5e7fa049 | |||
2c4b75aa6c | |||
4db894d061 | |||
5a3b03b69b |
@ -1,8 +0,0 @@
|
||||
// @ts-check
|
||||
const { extendConfig } = require("@aet/eslint-rules");
|
||||
|
||||
module.exports = extendConfig({
|
||||
rules: {
|
||||
"class-methods-use-this": "error",
|
||||
},
|
||||
});
|
@ -3,7 +3,11 @@
|
||||
Compile-run Tailwind compiler.
|
||||
|
||||
```tsx
|
||||
/// <reference types="@aet/tailwind/react-env" />
|
||||
|
||||
export function App() {
|
||||
return <div css="flex m-0"></div>;
|
||||
}
|
||||
|
||||
const Container = tw.div`flex m-0`;
|
||||
```
|
||||
|
9
eslint.config.js
Normal file
9
eslint.config.js
Normal file
@ -0,0 +1,9 @@
|
||||
// @ts-check
|
||||
import { extendConfig } from "@aet/eslint-rules";
|
||||
|
||||
export default await extendConfig({
|
||||
rules: {
|
||||
"class-methods-use-this": "error",
|
||||
"import-x/no-unresolved": ["error", { ignore: ["react"] }],
|
||||
},
|
||||
});
|
75
package.json
75
package.json
@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "@aet/tailwind",
|
||||
"version": "1.0.9",
|
||||
"version": "1.0.35",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "./scripts/index.ts",
|
||||
"test": "vitest",
|
||||
@ -13,7 +14,11 @@
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./package.json": "./package.json",
|
||||
"./classed": "./dist/classed.mjs",
|
||||
"./utils": "./dist/utils.js",
|
||||
"./css-to-js": "./dist/css-to-js.js",
|
||||
"./base": {
|
||||
"types": "./dist/base.d.ts"
|
||||
},
|
||||
"./µ": {
|
||||
"types": "./dist/macro.d.ts"
|
||||
},
|
||||
@ -25,49 +30,55 @@
|
||||
"./plugin/typography": "./dist/vendor/typography.js",
|
||||
"./macro": {
|
||||
"types": "./dist/macro.d.ts"
|
||||
},
|
||||
"./react-env": {
|
||||
"types": "./dist/react-env.d.ts"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aet/eslint-rules": "1.0.1-beta.24",
|
||||
"@aet/eslint-rules": "^2.0.52",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"@types/bun": "^1.1.6",
|
||||
"@types/bun": "^1.2.16",
|
||||
"@types/dedent": "^0.7.2",
|
||||
"@types/lodash": "^4.17.7",
|
||||
"@types/node": "^22.2.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^24.0.1",
|
||||
"@types/postcss-safe-parser": "^5.0.4",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/stylis": "^4.2.6",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/stylis": "^4.2.7",
|
||||
"@vitejs/plugin-react": "^4.5.2",
|
||||
"cli-highlight": "^2.1.11",
|
||||
"clsx": "^2.1.1",
|
||||
"colord": "^2.9.3",
|
||||
"css-what": "^6.1.0",
|
||||
"dedent": "^1.5.3",
|
||||
"dedent": "^1.6.0",
|
||||
"esbuild-register": "^3.6.0",
|
||||
"eslint": "^8.57.0",
|
||||
"nolyfill": "^1.0.39",
|
||||
"postcss-nested": "^6.2.0",
|
||||
"prettier": "^3.3.3",
|
||||
"tailwindcss": "^3.4.9",
|
||||
"tslib": "^2.6.3",
|
||||
"tsup": "^8.2.4",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.0",
|
||||
"vitest": "^2.0.5"
|
||||
"eslint": "^9.28.0",
|
||||
"nolyfill": "^1.0.44",
|
||||
"postcss-nested": "^7.0.2",
|
||||
"prettier": "^3.5.3",
|
||||
"react-refresh": "^0.17.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tslib": "^2.8.1",
|
||||
"tsup": "^8.5.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"vitest": "^3.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tailwindcss": "^3.4.3"
|
||||
"tailwindcss": "^3.4.17"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/core": "^7.27.4",
|
||||
"@emotion/hash": "^0.9.2",
|
||||
"esbuild": "^0.23.0",
|
||||
"clsx": "^2.1.1",
|
||||
"esbuild": "^0.25.5",
|
||||
"json5": "^2.2.3",
|
||||
"lodash": "^4.17.21",
|
||||
"postcss": "^8.4.41",
|
||||
"postcss-selector-parser": "^6.1.1",
|
||||
"stylis": "^4.3.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"postcss": "^8.5.5",
|
||||
"postcss-selector-parser": "^7.1.0",
|
||||
"stylis": "^4.3.6",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"type-fest": "^4.24.0"
|
||||
"type-fest": "^4.41.0"
|
||||
},
|
||||
"prettier": {
|
||||
"arrowParens": "avoid",
|
||||
@ -79,7 +90,11 @@
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"is-core-module": "npm:@nolyfill/is-core-module@^1"
|
||||
}
|
||||
"is-core-module": "npm:@nolyfill/is-core-module@^1",
|
||||
"@babel/types": "7.26.7"
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
"esbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
4304
pnpm-lock.yaml
generated
4304
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env bun
|
||||
#!/usr/bin/env tsx
|
||||
import { promises as fs } from "node:fs";
|
||||
|
||||
import { build, defineConfig } from "tsup";
|
||||
import { pick } from "lodash";
|
||||
|
||||
import pkg from "../package.json" with { type: "json" };
|
||||
|
||||
const tsupConfig = defineConfig({
|
||||
@ -11,19 +12,29 @@ const tsupConfig = defineConfig({
|
||||
dts: true,
|
||||
treeshake: true,
|
||||
platform: "node",
|
||||
format: "esm",
|
||||
banner: {
|
||||
js: "/* eslint-disable */",
|
||||
},
|
||||
define: {
|
||||
__PKG_NAME__: JSON.stringify(pkg.name),
|
||||
},
|
||||
});
|
||||
|
||||
await build({
|
||||
...tsupConfig,
|
||||
entry: ["src/utils.tsx", "src/css-to-js.ts"],
|
||||
outDir: "dist",
|
||||
external: ["react", "react/jsx-runtime", "clsx"],
|
||||
clean: true,
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
build({
|
||||
...tsupConfig,
|
||||
entry: ["src/classed.tsx"],
|
||||
entry: ["src/utils.tsx", "src/css-to-js.ts"],
|
||||
outDir: "dist",
|
||||
external: ["react", "react/jsx-runtime", "clsx"],
|
||||
format: "esm",
|
||||
clean: true,
|
||||
}),
|
||||
build({
|
||||
...tsupConfig,
|
||||
@ -45,21 +56,17 @@ await Promise.all([
|
||||
external: ["tailwindcss/plugin", "tailwindcss/colors", "tailwindcss/defaultTheme"],
|
||||
})
|
||||
),
|
||||
Bun.write(
|
||||
"dist/package.json",
|
||||
JSON.stringify(
|
||||
pick(pkg, ["name", "version", "license", "dependencies", "author"]),
|
||||
null,
|
||||
2
|
||||
)
|
||||
),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
fs.copyFile("README.md", "dist/README.md"),
|
||||
fs.copyFile("LICENSE.md", "dist/LICENSE.md"),
|
||||
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);
|
||||
|
101
scripts/update-tags.ts
Executable file
101
scripts/update-tags.ts
Executable file
@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env bun
|
||||
import fs from "node:fs";
|
||||
|
||||
const supportedTags = [
|
||||
"a",
|
||||
"abbr",
|
||||
"address",
|
||||
"article",
|
||||
"b",
|
||||
"bdi",
|
||||
"blockquote",
|
||||
"button",
|
||||
"caption",
|
||||
"cite",
|
||||
"code",
|
||||
"col",
|
||||
"colgroup",
|
||||
"dd",
|
||||
"del",
|
||||
"details",
|
||||
"dialog",
|
||||
"div",
|
||||
"dl",
|
||||
"dt",
|
||||
"em",
|
||||
"fieldset",
|
||||
"figcaption",
|
||||
"figure",
|
||||
"footer",
|
||||
"form",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"header",
|
||||
"hr",
|
||||
"i",
|
||||
"iframe",
|
||||
"img",
|
||||
"input",
|
||||
"ins",
|
||||
"kbd",
|
||||
"label",
|
||||
"legend",
|
||||
"li",
|
||||
"main",
|
||||
"mark",
|
||||
"menu",
|
||||
"meter",
|
||||
"nav",
|
||||
"ol",
|
||||
"output",
|
||||
"p",
|
||||
"picture",
|
||||
"pre",
|
||||
"progress",
|
||||
"q",
|
||||
"rt",
|
||||
"ruby",
|
||||
"section",
|
||||
"select",
|
||||
"span",
|
||||
"strong",
|
||||
"sub",
|
||||
"summary",
|
||||
"sup",
|
||||
"svg",
|
||||
"table",
|
||||
"tbody",
|
||||
"td",
|
||||
"textarea",
|
||||
"tfoot",
|
||||
"th",
|
||||
"thead",
|
||||
"time",
|
||||
"tr",
|
||||
"u",
|
||||
"ul",
|
||||
"var",
|
||||
"video",
|
||||
"wbr",
|
||||
].sort();
|
||||
|
||||
function replaceFile(file: string, search: string | RegExp, replace: string) {
|
||||
const content = fs.readFileSync(file, "utf-8");
|
||||
fs.writeFileSync(file, content.replace(search, replace));
|
||||
}
|
||||
|
||||
replaceFile(
|
||||
"src/macro.d.ts",
|
||||
/export type SupportedTag =(\n\s+\| "\w+")+;/,
|
||||
`export type SupportedTag =\n | "${supportedTags.join('"\n | "')}";`
|
||||
);
|
||||
|
||||
replaceFile(
|
||||
"src/babel/macro.ts",
|
||||
/const supportedTags = new Set<SupportedTag>\(\[(\n\s+"\w+",)+\n]\);/,
|
||||
`const supportedTags = new Set<SupportedTag>([\n "${supportedTags.join('",\n "')}",\n]);`
|
||||
);
|
27
src/__tests__/__snapshots__/emit.test.ts.snap
Normal file
27
src/__tests__/__snapshots__/emit.test.ts.snap
Normal file
@ -0,0 +1,27 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`emit > supports emitting as CSS module 1`] = `
|
||||
"// babel-tailwind:/Users/aet/Documents/Git/babel-tailwind/src/.temp-emit/index.module.css
|
||||
var index_default = {
|
||||
"tw-gqn2k6": "index_tw-gqn2k6",
|
||||
"tw-1qtvvjy": "index_tw-1qtvvjy"
|
||||
};
|
||||
|
||||
import { cx as _cx } from "@emotion/css";
|
||||
import { jsx } from "react/jsx-runtime";
|
||||
function Hello() {
|
||||
return /* @__PURE__ */ jsx("div", { className: _cx([index_default["tw-gqn2k6"], index_default["tw-1qtvvjy"]]), children: "Hello, world!" });
|
||||
}
|
||||
export {
|
||||
Hello
|
||||
};"
|
||||
`;
|
||||
|
||||
exports[`emit > supports emitting as CSS module 2`] = `
|
||||
".index_tw-gqn2k6 {
|
||||
text-align: center;
|
||||
}
|
||||
.index_tw-1qtvvjy:hover {
|
||||
font-weight: 600;
|
||||
}"
|
||||
`;
|
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
|
||||
|
||||
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";
|
||||
function Hello(props) {
|
||||
props = {
|
||||
|
@ -39,3 +39,14 @@ exports[`babel-tailwind > supports grouped tw 2`] = `
|
||||
font-weight: 600;
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`babel-tailwind > supports styled components usage 1`] = `
|
||||
"import { classed as _classed } from "@aet/tailwind/utils";
|
||||
var Div = _classed("div", "tw-gqn2k6");"
|
||||
`;
|
||||
|
||||
exports[`babel-tailwind > supports styled components usage 2`] = `
|
||||
".tw-gqn2k6 {
|
||||
text-align: center;
|
||||
}"
|
||||
`;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { describe, it } from "vitest";
|
||||
|
||||
import { getBuild, matchSnapshot } from "./utils";
|
||||
|
||||
describe("attr", () => {
|
||||
@ -60,115 +61,4 @@ describe("attr", () => {
|
||||
|
||||
matchSnapshot(files);
|
||||
});
|
||||
|
||||
it.only("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,5 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createPostCSS } from "../index";
|
||||
|
||||
import { createPostCSS, getClassName } from "../index";
|
||||
|
||||
import { getBuild, minCSS, name } from "./utils";
|
||||
|
||||
describe("babel-tailwind", () => {
|
||||
@ -8,7 +10,6 @@ describe("babel-tailwind", () => {
|
||||
it("supports importing tailwind/base", async () => {
|
||||
const postcss = createPostCSS({
|
||||
tailwindConfig: {},
|
||||
postCSSPlugins: [],
|
||||
});
|
||||
const base = await postcss("@tailwind base;");
|
||||
const { files } = await compileESBuild({
|
||||
@ -22,4 +23,23 @@ describe("babel-tailwind", () => {
|
||||
expect(files.js.text).toBe("");
|
||||
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}",`);
|
||||
});
|
||||
});
|
||||
|
26
src/__tests__/emit.test.ts
Normal file
26
src/__tests__/emit.test.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { describe, it } from "vitest";
|
||||
|
||||
import { getBuild, matchSnapshot } from "./utils";
|
||||
|
||||
describe("emit", () => {
|
||||
const compileESBuild = getBuild("emit");
|
||||
|
||||
it.only("supports emitting as CSS module", async () => {
|
||||
const { files } = await compileESBuild({
|
||||
clsx: "emotion",
|
||||
expectFiles: 2,
|
||||
cssModules: true,
|
||||
javascript: /* tsx */ `
|
||||
export function Hello() {
|
||||
return (
|
||||
<div css={["text-center", { hover: "font-semibold" }]}>
|
||||
Hello, world!
|
||||
</div>
|
||||
);
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
matchSnapshot(files);
|
||||
});
|
||||
});
|
@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getBuild } from "./utils";
|
||||
|
||||
describe("babel-tailwind", () => {
|
||||
|
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);
|
||||
});
|
||||
});
|
@ -1,6 +1,8 @@
|
||||
/* eslint-disable unicorn/string-content */
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getClassName } from "../index";
|
||||
|
||||
import { getBuild } from "./utils";
|
||||
|
||||
describe("merges with existing className attribute", () => {
|
||||
@ -43,8 +45,94 @@ describe("merges with existing className attribute", () => {
|
||||
|
||||
const clsName = getClassName("text-center");
|
||||
expect(files.js.text).toContain(
|
||||
`className: ({\n isEntering\n }) => _cx(isEntering ? "enter" : "exit", "${clsName}")`
|
||||
`className: ({\n isEntering\n }) => _cx("${clsName}", isEntering ? "enter" : "exit")`
|
||||
);
|
||||
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 () => {
|
||||
const { files } = await compileESBuild({
|
||||
clsx: "clsx",
|
||||
expectFiles: 2,
|
||||
javascript: /* tsx */ `
|
||||
import { clsx } from "clsx";
|
||||
|
||||
export function Hello(className) {
|
||||
return (
|
||||
<div className={clsx("font-semibold", className)} css="text-center" />
|
||||
);
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
expect([...files.js.text.match(/from "clsx"/g)!]).toHaveLength(1);
|
||||
expect(files.js.text).toContain(
|
||||
'{ className: clsx("tw-gqn2k6", "font-semibold", className)'
|
||||
);
|
||||
});
|
||||
|
||||
it("does not reuse invalid clsx", async () => {
|
||||
const { files } = await compileESBuild({
|
||||
clsx: "clsx",
|
||||
expectFiles: 2,
|
||||
javascript: /* tsx */ `
|
||||
import { clsx } from "clsx";
|
||||
|
||||
export function Hello(className) {
|
||||
let clsx = () => {};
|
||||
return (
|
||||
<div className={clsx("font-semibold", className)} css="text-center" />
|
||||
);
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
expect(files.js.text).toContain(
|
||||
'className: _cx("tw-gqn2k6", clsx("font-semibold", className))'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getBuild, matchSnapshot } from "./utils";
|
||||
|
||||
describe("options", () => {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { describe, it } from "vitest";
|
||||
|
||||
import { getBuild, matchSnapshot } from "./utils";
|
||||
|
||||
describe("babel-tailwind", () => {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { describe, it } from "vitest";
|
||||
|
||||
import { getBuild, matchSnapshot } from "./utils";
|
||||
|
||||
describe("babel-tailwind", () => {
|
||||
@ -40,4 +41,20 @@ describe("babel-tailwind", () => {
|
||||
|
||||
matchSnapshot(files);
|
||||
});
|
||||
|
||||
it("supports styled components usage", async () => {
|
||||
const { files } = await compileESBuild({
|
||||
clsx: "emotion",
|
||||
expectFiles: 2,
|
||||
javascript: `
|
||||
import {tw} from "@aet/tailwind/macro";
|
||||
|
||||
const Div = tw.div\`
|
||||
text-center
|
||||
\`;
|
||||
`,
|
||||
});
|
||||
|
||||
matchSnapshot(files);
|
||||
});
|
||||
});
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { afterEach, beforeEach, expect } from "vitest";
|
||||
import * as esbuild from "esbuild";
|
||||
|
||||
import type { PluginItem } from "@babel/core";
|
||||
import dedent from "dedent";
|
||||
import * as esbuild from "esbuild";
|
||||
import { afterEach, beforeEach, expect } from "vitest";
|
||||
|
||||
import { type TailwindPluginOptions, babelPlugin, getTailwindPlugins } from "../index";
|
||||
|
||||
export { name } from "../../package.json" with { type: "json" };
|
||||
@ -33,12 +36,14 @@ export function getBuild(name: string) {
|
||||
return async function compileESBuild({
|
||||
javascript,
|
||||
esbuild: esbuildOptions,
|
||||
babelPlugins,
|
||||
expectFiles,
|
||||
...options
|
||||
}: Omit<TailwindPluginOptions, "compile"> & {
|
||||
esbuild?: esbuild.BuildOptions;
|
||||
javascript: string;
|
||||
expectFiles?: number;
|
||||
babelPlugins?: PluginItem[];
|
||||
}) {
|
||||
const tailwind = getTailwindPlugins({
|
||||
tailwindConfig: {},
|
||||
@ -47,11 +52,22 @@ export function getBuild(name: string) {
|
||||
const result = await esbuild.build({
|
||||
bundle: true,
|
||||
write: false,
|
||||
external: ["react/jsx-runtime", "@emotion/css", "clsx", "tslib"],
|
||||
external: [
|
||||
"react",
|
||||
"react/jsx-runtime",
|
||||
"react-aria-components",
|
||||
"@emotion/css",
|
||||
"clsx",
|
||||
"tslib",
|
||||
"@aet/tailwind/utils",
|
||||
],
|
||||
outdir: "dist",
|
||||
format: "esm",
|
||||
entryPoints: [await write("index.tsx", dedent(javascript))],
|
||||
plugins: [babelPlugin({ plugins: [tailwind.babel()] }), tailwind.esbuild()],
|
||||
plugins: [
|
||||
babelPlugin({ plugins: [tailwind.babel(), ...(babelPlugins ?? [])] }),
|
||||
tailwind.esbuild(),
|
||||
],
|
||||
...esbuildOptions,
|
||||
});
|
||||
|
||||
|
@ -1,536 +0,0 @@
|
||||
import { basename, dirname, extname, join } from "node:path";
|
||||
import type b 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, type StyleMapEntry, macroNames } from "./shared";
|
||||
import { type ResolveTailwindOptions, getClassName } from "./index";
|
||||
|
||||
export type ClassNameCollector = (path: string, entries: StyleMapEntry[]) => void;
|
||||
type BabelTypes = typeof b.types;
|
||||
type Type = "css" | "js";
|
||||
|
||||
export function babelTailwind(
|
||||
options: ResolveTailwindOptions,
|
||||
onCollect: ClassNameCollector | undefined
|
||||
) {
|
||||
const {
|
||||
styleMap,
|
||||
clsx,
|
||||
getClassName: getClass = getClassName,
|
||||
jsxAttributeAction = "delete",
|
||||
jsxAttributeName = "css",
|
||||
vite: bustCache,
|
||||
} = options;
|
||||
|
||||
type BabelPluginUtils = ReturnType<typeof getUtils>;
|
||||
|
||||
function getUtils(path: NodePath<t.Program>, state: b.PluginPass, t: BabelTypes) {
|
||||
let cx: t.Identifier;
|
||||
let tslibImport: t.Identifier;
|
||||
let styleImport: t.Identifier;
|
||||
|
||||
const cssMap = new Map<string, StyleMapEntry>();
|
||||
const jsMap = new Map<string, StyleMapEntry>();
|
||||
|
||||
function getStyleImport() {
|
||||
styleImport ??= path.scope.generateUidIdentifier("styles");
|
||||
return t.cloneNode(styleImport);
|
||||
}
|
||||
|
||||
return {
|
||||
getClass(type: Type, value: string) {
|
||||
return type === "css" ? getClass(value) : "tw_" + hash(value);
|
||||
},
|
||||
|
||||
sliceText: (node: t.Node): SourceLocation => ({
|
||||
filename: state.filename!,
|
||||
start: node.loc!.start,
|
||||
end: node.loc!.end,
|
||||
text: state.file.code
|
||||
.split("\n")
|
||||
.slice(node.loc!.start.line - 1, node.loc!.end.line)
|
||||
.join("\n"),
|
||||
}),
|
||||
|
||||
recordIfAbsent(type: Type, entry: StyleMapEntry) {
|
||||
const map = type === "css" ? cssMap : jsMap;
|
||||
if (!map.has(entry.key)) {
|
||||
map.set(entry.key, entry);
|
||||
}
|
||||
},
|
||||
|
||||
replaceWithImport({
|
||||
type,
|
||||
path,
|
||||
className,
|
||||
}: {
|
||||
type: Type;
|
||||
path: NodePath;
|
||||
className: string;
|
||||
}) {
|
||||
if (type === "css") {
|
||||
path.replaceWith(t.stringLiteral(className));
|
||||
} else {
|
||||
const styleImportId = getStyleImport();
|
||||
path.replaceWith(
|
||||
t.memberExpression(styleImportId, t.stringLiteral(className), true)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
getCx: () => {
|
||||
if (cx == null) {
|
||||
cx = path.scope.generateUidIdentifier("cx");
|
||||
path.node.body.unshift(getClsxImport(t, cx, clsx));
|
||||
}
|
||||
return t.cloneNode(cx);
|
||||
},
|
||||
|
||||
getTSlibImport: () => {
|
||||
if (tslibImport == null) {
|
||||
tslibImport = path.scope.generateUidIdentifier("tslib");
|
||||
path.node.body.unshift(
|
||||
t.importDeclaration(
|
||||
[t.importNamespaceSpecifier(tslibImport)],
|
||||
t.stringLiteral("tslib")
|
||||
)
|
||||
);
|
||||
}
|
||||
return t.cloneNode(tslibImport);
|
||||
},
|
||||
|
||||
finish(node: t.Program) {
|
||||
const { filename } = state;
|
||||
if (!cssMap.size && !jsMap.size) return;
|
||||
invariant(filename, "babel: missing state.filename");
|
||||
|
||||
if (cssMap.size) {
|
||||
const cssName = basename(filename, extname(filename)) + ".css";
|
||||
const path = join(dirname(filename), cssName);
|
||||
const value = Array.from(cssMap.values());
|
||||
const importee = `tailwind:./${cssName}` + getSuffix(bustCache, value);
|
||||
|
||||
node.body.unshift(t.importDeclaration([], t.stringLiteral(importee)));
|
||||
|
||||
styleMap.set(path, value);
|
||||
onCollect?.(path, value);
|
||||
}
|
||||
|
||||
if (jsMap.size) {
|
||||
const jsName = basename(filename, extname(filename)) + ".tailwindStyle.js";
|
||||
const path = join(dirname(filename), jsName);
|
||||
const value = Array.from(jsMap.values());
|
||||
const importee = `tailwind:./${jsName}` + getSuffix(bustCache, value);
|
||||
|
||||
node.body.unshift(
|
||||
t.importDeclaration(
|
||||
[t.importNamespaceSpecifier(getStyleImport())],
|
||||
t.stringLiteral(importee)
|
||||
)
|
||||
);
|
||||
styleMap.set(path, value);
|
||||
onCollect?.(path, value);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return definePlugin<BabelPluginUtils>(({ types: t }) => ({
|
||||
Program: {
|
||||
enter(path, state) {
|
||||
const _ = getUtils(path, state, t);
|
||||
Object.assign(state, _);
|
||||
|
||||
for (const { callee, imported, prefix } of getMacros(t, path, macroNames).map(
|
||||
macro => mapMacro(t, macro)
|
||||
)) {
|
||||
const type = imported === "tw" ? "css" : imported === "tws" ? "js" : undefined;
|
||||
if (!type) continue;
|
||||
|
||||
if (isNodePath(callee, t.isTaggedTemplateExpression)) {
|
||||
const { node } = callee;
|
||||
const { quasi } = node;
|
||||
|
||||
invariant(
|
||||
!quasi.expressions.length,
|
||||
`Macro call should not contain expressions`
|
||||
);
|
||||
|
||||
const value = quasi.quasis[0].value.cooked;
|
||||
if (value) {
|
||||
const list = trimPrefix(value, prefix ? prefix + ":" : undefined);
|
||||
const className = _.getClass(type, list.join(" "));
|
||||
_.recordIfAbsent(type, {
|
||||
key: className,
|
||||
classNames: list,
|
||||
location: _.sliceText(node),
|
||||
});
|
||||
_.replaceWithImport({
|
||||
type,
|
||||
path: callee,
|
||||
className: addIf(className, list.includes("group") && " group"),
|
||||
});
|
||||
}
|
||||
} else if (isNodePath(callee, t.isCallExpression)) {
|
||||
const { node } = callee;
|
||||
if (!t.isIdentifier(node.callee)) continue;
|
||||
|
||||
const list = callee.get("arguments").flatMap(evaluateArgs);
|
||||
const className = getClass(list.join(" "));
|
||||
_.recordIfAbsent(type, {
|
||||
key: className,
|
||||
classNames: list,
|
||||
location: _.sliceText(node),
|
||||
});
|
||||
_.replaceWithImport({
|
||||
type,
|
||||
path: callee,
|
||||
className: addIf(className, list.includes("group") && " group"),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
exit({ node }, _) {
|
||||
_.finish(node);
|
||||
},
|
||||
},
|
||||
|
||||
JSXAttribute(path, _) {
|
||||
const { name } = path.node;
|
||||
if (name.name !== jsxAttributeName) return;
|
||||
|
||||
const valuePath = path.get("value");
|
||||
if (!valuePath.node) return;
|
||||
|
||||
const copy =
|
||||
jsxAttributeAction === "delete" ? undefined : t.cloneNode(valuePath.node, true);
|
||||
|
||||
const parent = path.parent as t.JSXOpeningElement;
|
||||
const classNameAttribute = parent.attributes.find(
|
||||
(attr): attr is t.JSXAttribute =>
|
||||
t.isJSXAttribute(attr) && attr.name.name === "className"
|
||||
);
|
||||
|
||||
matchPath(valuePath, go => ({
|
||||
StringLiteral(path) {
|
||||
const { node } = path;
|
||||
const { value } = node;
|
||||
const trimmed = trim(value);
|
||||
if (trimmed.length) {
|
||||
const className = getClass(trimmed.join(" "));
|
||||
_.recordIfAbsent("css", {
|
||||
key: className,
|
||||
classNames: trimmed,
|
||||
location: _.sliceText(node),
|
||||
});
|
||||
path.replaceWith(t.stringLiteral(className));
|
||||
}
|
||||
},
|
||||
ArrayExpression(path) {
|
||||
for (const element of path.get("elements")) {
|
||||
go(element);
|
||||
}
|
||||
},
|
||||
ObjectExpression(path) {
|
||||
const trimmed = evaluateArgs(path);
|
||||
const className = getClass(trimmed.join(" "));
|
||||
_.recordIfAbsent("css", {
|
||||
key: className,
|
||||
classNames: trimmed,
|
||||
location: _.sliceText(path.node),
|
||||
});
|
||||
path.replaceWith(t.stringLiteral(className));
|
||||
},
|
||||
JSXExpressionContainer(path) {
|
||||
go(path.get("expression"));
|
||||
},
|
||||
ConditionalExpression(path) {
|
||||
go(path.get("consequent"));
|
||||
go(path.get("alternate"));
|
||||
},
|
||||
LogicalExpression(path) {
|
||||
go(path.get("right"));
|
||||
},
|
||||
CallExpression(path) {
|
||||
for (const arg of path.get("arguments")) {
|
||||
go(arg);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
let valuePathNode = extractJSXContainer(valuePath.node);
|
||||
if (
|
||||
t.isArrayExpression(valuePathNode) &&
|
||||
valuePathNode.elements.every(node => t.isStringLiteral(node))
|
||||
) {
|
||||
valuePathNode = t.stringLiteral(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
(valuePathNode.elements as t.StringLiteral[]).map(node => node.value).join(" ")
|
||||
);
|
||||
}
|
||||
|
||||
if (classNameAttribute) {
|
||||
const attrValue = classNameAttribute.value!;
|
||||
const wrap = (originalValue: b.types.Expression) =>
|
||||
t.callExpression(_.getCx(), [originalValue, valuePathNode]);
|
||||
|
||||
// If both are string literals, we can merge them directly here
|
||||
if (t.isStringLiteral(attrValue) && t.isStringLiteral(valuePathNode)) {
|
||||
attrValue.value +=
|
||||
(attrValue.value.at(-1) === " " ? "" : " ") + valuePathNode.value;
|
||||
} else {
|
||||
const internalAttrValue = extractJSXContainer(attrValue);
|
||||
if (
|
||||
t.isArrowFunctionExpression(internalAttrValue) &&
|
||||
!t.isBlockStatement(internalAttrValue.body)
|
||||
) {
|
||||
internalAttrValue.body = wrap(internalAttrValue.body);
|
||||
} else {
|
||||
classNameAttribute.value = t.jsxExpressionContainer(wrap(internalAttrValue));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const wrap = (originalValue: b.types.Expression) =>
|
||||
t.callExpression(_.getCx(), [valuePathNode, originalValue]);
|
||||
|
||||
const rest = parent.attributes.filter(attr => t.isJSXSpreadAttribute(attr));
|
||||
let arg;
|
||||
if (rest.length === 1 && (arg = rest[0].argument) && t.isIdentifier(arg)) {
|
||||
// props from argument and not modified anywhere
|
||||
const scope = path.scope.getBinding(arg.name);
|
||||
let index: number;
|
||||
const node = scope?.path.node;
|
||||
if (
|
||||
scope &&
|
||||
!scope.constantViolations.length &&
|
||||
t.isFunctionDeclaration(scope.path.parent) &&
|
||||
(index = (scope.path.parent.params as t.Node[]).indexOf(node!)) !== -1 &&
|
||||
(t.isIdentifier(node) || t.isObjectPattern(node))
|
||||
) {
|
||||
const clsVar = path.scope.generateUidIdentifier("className");
|
||||
if (t.isIdentifier(node)) {
|
||||
scope.path.parent.params[index] = t.objectPattern([
|
||||
t.objectProperty(t.identifier("className"), clsVar),
|
||||
t.restElement(node),
|
||||
]);
|
||||
} else {
|
||||
node.properties.unshift(
|
||||
t.objectProperty(t.identifier("className"), clsVar)
|
||||
);
|
||||
}
|
||||
|
||||
parent.attributes.push(
|
||||
t.jsxAttribute(
|
||||
t.jsxIdentifier("className"),
|
||||
t.jsxExpressionContainer(wrap(clsVar))
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const tslibImport = _.getTSlibImport();
|
||||
rest[0].argument = t.callExpression(
|
||||
t.memberExpression(tslibImport, t.identifier("__rest")),
|
||||
[arg, t.arrayExpression([t.stringLiteral("className")])]
|
||||
);
|
||||
|
||||
parent.attributes.push(
|
||||
t.jsxAttribute(
|
||||
t.jsxIdentifier("className"),
|
||||
t.jsxExpressionContainer(
|
||||
wrap(t.memberExpression(arg, t.identifier("className")))
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const containerValue = t.isStringLiteral(valuePathNode)
|
||||
? valuePathNode
|
||||
: t.callExpression(_.getCx(), [valuePathNode]);
|
||||
|
||||
parent.attributes.push(
|
||||
t.jsxAttribute(
|
||||
t.jsxIdentifier("className"),
|
||||
t.jsxExpressionContainer(containerValue)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (jsxAttributeAction === "delete") {
|
||||
path.remove();
|
||||
} else {
|
||||
path.node.value = copy!;
|
||||
if (Array.isArray(jsxAttributeAction) && jsxAttributeAction[0] === "rename") {
|
||||
path.node.name.name = jsxAttributeAction[1];
|
||||
}
|
||||
}
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
function getClsxImport(t: BabelTypes, cx: t.Identifier, clsx: string) {
|
||||
switch (clsx) {
|
||||
case "emotion":
|
||||
return t.importDeclaration(
|
||||
[t.importSpecifier(cx, t.identifier("cx"))],
|
||||
t.stringLiteral("@emotion/css")
|
||||
);
|
||||
case "clsx":
|
||||
return t.importDeclaration([t.importDefaultSpecifier(cx)], t.stringLiteral("clsx"));
|
||||
case "classnames":
|
||||
return t.importDeclaration(
|
||||
[t.importDefaultSpecifier(cx)],
|
||||
t.stringLiteral("classnames")
|
||||
);
|
||||
default:
|
||||
throw new Error("Unknown clsx library");
|
||||
}
|
||||
}
|
||||
|
||||
function evaluateArgs(path: NodePath) {
|
||||
const { confident, value } = path.evaluate();
|
||||
invariant(confident, "Argument cannot be statically evaluated");
|
||||
|
||||
if (typeof value === "string") {
|
||||
return trim(value);
|
||||
}
|
||||
|
||||
if (isPlainObject(value)) {
|
||||
return flatMapEntries(value, (classes, modifier) => {
|
||||
if (modifier === "data" && isPlainObject(classes)) {
|
||||
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}]:`)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
invariant(
|
||||
typeof classes === "string",
|
||||
`Value for "${modifier}" should be a string`
|
||||
);
|
||||
return trimPrefix(classes, modifier + ":");
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error("Invalid argument type");
|
||||
}
|
||||
|
||||
function getName(t: BabelTypes, exp: t.Node) {
|
||||
if (t.isIdentifier(exp)) {
|
||||
return exp.name;
|
||||
} else if (t.isStringLiteral(exp)) {
|
||||
return exp.value;
|
||||
}
|
||||
}
|
||||
|
||||
function getMacros(
|
||||
t: BabelTypes,
|
||||
programPath: NodePath<t.Program>,
|
||||
importSources: string[]
|
||||
) {
|
||||
const importDecs = programPath
|
||||
.get("body")
|
||||
.filter(x => isNodePath(x, t.isImportDeclaration))
|
||||
.filter(x => importSources.includes(x.node.source.value));
|
||||
|
||||
const macros = importDecs
|
||||
.flatMap(x => x.get("specifiers"))
|
||||
.map(x => {
|
||||
const local = x.get("local");
|
||||
if (isNodePath(x, t.isImportNamespaceSpecifier)) {
|
||||
return local.scope
|
||||
.getOwnBinding(local.node.name)!
|
||||
.referencePaths.map(p => p.parentPath)
|
||||
.filter(p => isNodePath(p, t.isMemberExpression))
|
||||
.map(p => ({
|
||||
local: p,
|
||||
imported: getName(t, p.node.property)!,
|
||||
}))
|
||||
.filter(p => p.imported);
|
||||
} else if (t.isImportSpecifier(x.node)) {
|
||||
const imported = x.node.imported;
|
||||
return local.scope.getOwnBinding(local.node.name)!.referencePaths.map(p => ({
|
||||
local: p as NodePath<t.Identifier>,
|
||||
imported: getName(t, imported)!,
|
||||
}));
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
.flat(1);
|
||||
|
||||
for (const x of importDecs) {
|
||||
x.remove();
|
||||
}
|
||||
|
||||
return macros;
|
||||
}
|
||||
|
||||
function mapMacro(t: BabelTypes, macro: ReturnType<typeof getMacros>[number]) {
|
||||
let callee = macro.local.parentPath;
|
||||
const prefix: string[] = [];
|
||||
|
||||
while (isNodePath(callee, t.isMemberExpression)) {
|
||||
invariant(t.isIdentifier(callee.node.property), "Invalid member expression");
|
||||
prefix.unshift(
|
||||
callee.node.property.name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()
|
||||
);
|
||||
callee = callee.parentPath;
|
||||
}
|
||||
|
||||
return {
|
||||
callee,
|
||||
imported: macro.imported,
|
||||
prefix: prefix.length ? prefix.join(":") : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const definePlugin =
|
||||
<T>(fn: (runtime: typeof b) => b.Visitor<b.PluginPass & T>) =>
|
||||
(runtime: typeof b) => {
|
||||
const plugin: b.PluginObj<b.PluginPass & T> = {
|
||||
visitor: fn(runtime),
|
||||
};
|
||||
return plugin as b.PluginObj;
|
||||
};
|
||||
|
||||
const extractJSXContainer = (attr: NonNullable<t.JSXAttribute["value"]>): t.Expression =>
|
||||
attr.type === "JSXExpressionContainer" ? (attr.expression as t.Expression) : attr;
|
||||
|
||||
function matchPath(
|
||||
nodePath: NodePath<t.Node | null | undefined>,
|
||||
fns: (dig: (nodePath: NodePath<t.Node | null | undefined>) => void) => b.Visitor
|
||||
) {
|
||||
if (!nodePath.node) return;
|
||||
const fn = fns(path => matchPath(path, fns))[nodePath.node.type] as any;
|
||||
fn?.(nodePath);
|
||||
}
|
||||
|
||||
function addIf(text: string, suffix: string | false) {
|
||||
return suffix ? text + suffix : text;
|
||||
}
|
||||
|
||||
const isNodePath = <T extends t.Node>(
|
||||
nodePath: NodePath<t.Node | null | undefined> | null,
|
||||
predicate: (node: t.Node) => node is T
|
||||
): nodePath is NodePath<T> => Boolean(nodePath?.node && predicate(nodePath.node));
|
||||
|
||||
function getSuffix(add: boolean | undefined, entries: StyleMapEntry[]) {
|
||||
if (!add) return "";
|
||||
|
||||
const cacheKey = hash(entries.map(x => x.classNames).join(","));
|
||||
return `?${cacheKey}`;
|
||||
}
|
||||
|
||||
const trim = (value: string) =>
|
||||
value.replace(/\s+/g, " ").trim().split(" ").filter(Boolean);
|
||||
const trimPrefix = (cls: string, prefix = "") => trim(cls).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));
|
544
src/babel/index.ts
Normal file
544
src/babel/index.ts
Normal file
@ -0,0 +1,544 @@
|
||||
import { basename, dirname, extname, join } from "node:path";
|
||||
|
||||
import type b from "@babel/core";
|
||||
import { type Node, type NodePath, type types as t } from "@babel/core";
|
||||
import hash from "@emotion/hash";
|
||||
import { memoize } from "lodash-es";
|
||||
import invariant from "tiny-invariant";
|
||||
|
||||
import { type ResolveTailwindOptions, getClassName } from "../index";
|
||||
import { type SourceLocation, type StyleMapEntry, utilsName } from "../shared";
|
||||
|
||||
import { handleMacro } from "./macro";
|
||||
import { evaluateArgs, trim } from "./utils";
|
||||
|
||||
export type ClassNameCollector = (path: string, entries: StyleMapEntry[]) => void;
|
||||
type BabelTypes = typeof b.types;
|
||||
type Type = "css" | "js";
|
||||
|
||||
type Scope = ReturnType<typeof NodePath.prototype.getScope>;
|
||||
|
||||
export type BabelPluginUtils = ReturnType<typeof getUtils>;
|
||||
|
||||
interface Import {
|
||||
source: string;
|
||||
specifiers: {
|
||||
local: string;
|
||||
imported: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
function getUtils({
|
||||
path,
|
||||
state,
|
||||
t,
|
||||
options,
|
||||
onCollect,
|
||||
}: {
|
||||
path: NodePath<t.Program>;
|
||||
state: b.PluginPass;
|
||||
t: BabelTypes;
|
||||
options: ResolveTailwindOptions;
|
||||
onCollect: ClassNameCollector | undefined;
|
||||
}) {
|
||||
const {
|
||||
styleMap,
|
||||
clsx,
|
||||
getClassName: getClass = getClassName,
|
||||
vite: bustCache,
|
||||
cssModules,
|
||||
} = options;
|
||||
|
||||
let cx: t.Identifier;
|
||||
|
||||
const cssMap = new Map<string, StyleMapEntry>();
|
||||
const jsMap = new Map<string, StyleMapEntry>();
|
||||
|
||||
const imports: Import[] = path.node.body
|
||||
.filter(node => t.isImportDeclaration(node))
|
||||
.map(i => ({
|
||||
source: i.source.value,
|
||||
specifiers: i.specifiers
|
||||
.filter(x => t.isImportSpecifier(x))
|
||||
.filter(x => x.importKind === "value")
|
||||
.map(x => ({
|
||||
local: x.local.name,
|
||||
imported: t.isStringLiteral(x.imported) ? x.imported.value : x.imported.name,
|
||||
})),
|
||||
}));
|
||||
|
||||
let existingCx: string | undefined;
|
||||
switch (clsx) {
|
||||
case "emotion":
|
||||
existingCx = imports
|
||||
.find(i => i.source === "@emotion/css")
|
||||
?.specifiers.find(s => s.imported === "cx")?.local;
|
||||
break;
|
||||
case "clsx":
|
||||
existingCx = imports
|
||||
.find(i => i.source === "clsx")
|
||||
?.specifiers.find(s => s.imported === "clsx")?.local;
|
||||
break;
|
||||
case "classnames":
|
||||
existingCx = imports.find(i => i.source === "classnames")?.specifiers[0]?.local;
|
||||
break;
|
||||
}
|
||||
|
||||
function reuseImport(scope: Scope) {
|
||||
if (
|
||||
existingCx &&
|
||||
scope.getBinding(existingCx) === path.scope.getBinding(existingCx)
|
||||
) {
|
||||
return t.identifier(existingCx);
|
||||
}
|
||||
}
|
||||
|
||||
function cacheNode<N extends Node>(fn: () => N) {
|
||||
let cache: N | undefined;
|
||||
return Object.assign(
|
||||
(): N => {
|
||||
cache ??= fn();
|
||||
return t.cloneNode(cache);
|
||||
},
|
||||
{
|
||||
getCache() {
|
||||
return cache;
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const getStyleImport = cacheNode(() => path.scope.generateUidIdentifier("styles"));
|
||||
const getCssModuleImport = cacheNode(() =>
|
||||
path.scope.generateUidIdentifier("cssModule")
|
||||
);
|
||||
|
||||
const getUtilsImport = memoize(() => {
|
||||
const importDecl = t.importDeclaration([], t.stringLiteral(utilsName));
|
||||
path.node.body.unshift(importDecl);
|
||||
return importDecl;
|
||||
});
|
||||
|
||||
return {
|
||||
program: path,
|
||||
existingCx,
|
||||
|
||||
getClass(type: Type, value: string) {
|
||||
return type === "css" ? getClass(value) : "tw_" + hash(value);
|
||||
},
|
||||
|
||||
sliceText: (node: t.Node): SourceLocation => ({
|
||||
filename: state.filename!,
|
||||
start: node.loc!.start,
|
||||
end: node.loc!.end,
|
||||
text: state.file.code
|
||||
.split("\n")
|
||||
.slice(node.loc!.start.line - 1, node.loc!.end.line)
|
||||
.join("\n"),
|
||||
}),
|
||||
|
||||
recordIfAbsent(type: Type, entry: StyleMapEntry) {
|
||||
const map = type === "css" ? cssMap : jsMap;
|
||||
if (!map.has(entry.key)) {
|
||||
map.set(entry.key, entry);
|
||||
}
|
||||
},
|
||||
|
||||
replaceWithImport({
|
||||
type,
|
||||
path,
|
||||
className,
|
||||
}: {
|
||||
type: Type;
|
||||
path: NodePath;
|
||||
className: string;
|
||||
}) {
|
||||
if (type === "css") {
|
||||
path.replaceWith(t.stringLiteral(className));
|
||||
} else {
|
||||
const styleImportId = getStyleImport();
|
||||
path.replaceWith(
|
||||
t.memberExpression(styleImportId, t.stringLiteral(className), true)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
getCx: (localScope: Scope) => {
|
||||
if (cx == null) {
|
||||
const reuse = reuseImport(localScope);
|
||||
if (reuse) return reuse;
|
||||
|
||||
cx = path.scope.generateUidIdentifier("cx");
|
||||
// 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);
|
||||
},
|
||||
|
||||
getTSlibImport: cacheNode(() => {
|
||||
const tslibImport = path.scope.generateUidIdentifier("tslib");
|
||||
path.node.body.push(
|
||||
t.importDeclaration(
|
||||
[t.importNamespaceSpecifier(tslibImport)],
|
||||
t.stringLiteral("tslib")
|
||||
)
|
||||
);
|
||||
return tslibImport;
|
||||
}),
|
||||
|
||||
getClsCompose: cacheNode(() => {
|
||||
const clsComposeImport = path.scope.generateUidIdentifier("composeClassName");
|
||||
getUtilsImport().specifiers.push(
|
||||
t.importSpecifier(clsComposeImport, t.identifier("composeClassName"))
|
||||
);
|
||||
return clsComposeImport;
|
||||
}),
|
||||
|
||||
getClassedImport: cacheNode(() => {
|
||||
const classedImport = path.scope.generateUidIdentifier("classed");
|
||||
getUtilsImport().specifiers.push(
|
||||
t.importSpecifier(classedImport, t.identifier("classed"))
|
||||
);
|
||||
return classedImport;
|
||||
}),
|
||||
|
||||
getCssModuleImport,
|
||||
|
||||
getClassNameValue: (className: string) => {
|
||||
const validId = t.isValidIdentifier(className);
|
||||
return cssModules
|
||||
? t.memberExpression(
|
||||
getCssModuleImport(),
|
||||
validId ? t.identifier(className) : t.stringLiteral(className),
|
||||
!validId
|
||||
)
|
||||
: t.stringLiteral(className);
|
||||
},
|
||||
|
||||
finish(node: t.Program) {
|
||||
const { filename } = state;
|
||||
if (!cssMap.size && !jsMap.size) return;
|
||||
invariant(filename, "babel: missing state.filename");
|
||||
|
||||
const cssModuleImport = getCssModuleImport.getCache();
|
||||
if (cssMap.size) {
|
||||
const cssName =
|
||||
basename(filename, extname(filename)) +
|
||||
(cssModuleImport ? ".module" : "") +
|
||||
".css";
|
||||
const path = join(dirname(filename), cssName);
|
||||
const value = Array.from(cssMap.values());
|
||||
|
||||
if (cssModuleImport) {
|
||||
const importee = `tailwind:./${cssName}` + getSuffix(bustCache, value);
|
||||
node.body.unshift(
|
||||
t.importDeclaration(
|
||||
[t.importDefaultSpecifier(cssModuleImport)],
|
||||
t.stringLiteral(importee)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const importee = `tailwind:./${cssName}` + getSuffix(bustCache, value);
|
||||
node.body.unshift(t.importDeclaration([], t.stringLiteral(importee)));
|
||||
}
|
||||
|
||||
styleMap.set(path, value);
|
||||
onCollect?.(path, value);
|
||||
}
|
||||
|
||||
if (jsMap.size) {
|
||||
const jsName = basename(filename, extname(filename)) + ".tailwindStyle.js";
|
||||
const path = join(dirname(filename), jsName);
|
||||
const value = Array.from(jsMap.values());
|
||||
const importee = `tailwind:./${jsName}` + getSuffix(bustCache, value);
|
||||
|
||||
node.body.unshift(
|
||||
t.importDeclaration(
|
||||
[t.importNamespaceSpecifier(getStyleImport())],
|
||||
t.stringLiteral(importee)
|
||||
)
|
||||
);
|
||||
styleMap.set(path, value);
|
||||
onCollect?.(path, value);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function babelTailwind(
|
||||
options: ResolveTailwindOptions,
|
||||
onCollect: ClassNameCollector | undefined
|
||||
) {
|
||||
const {
|
||||
getClassName: getClass = getClassName,
|
||||
jsxAttributeAction = "delete",
|
||||
jsxAttributeName = "css",
|
||||
composeRenderProps,
|
||||
} = options;
|
||||
|
||||
return definePlugin<BabelPluginUtils>(({ types: t }) => ({
|
||||
Program: {
|
||||
enter(path, state) {
|
||||
const _ = getUtils({ path, state, t, options, onCollect });
|
||||
Object.assign(state, _);
|
||||
handleMacro({ t, path, _ });
|
||||
},
|
||||
|
||||
exit({ node }, _) {
|
||||
_.finish(node);
|
||||
},
|
||||
},
|
||||
|
||||
JSXAttribute(path, _) {
|
||||
const { name } = path.node;
|
||||
if (name.name !== jsxAttributeName) return;
|
||||
|
||||
const valuePath = path.get("value");
|
||||
if (!valuePath.node) return;
|
||||
|
||||
const copy =
|
||||
jsxAttributeAction === "delete" ? undefined : t.cloneNode(valuePath.node, true);
|
||||
|
||||
const parent = path.parent as t.JSXOpeningElement;
|
||||
const classNameAttribute = parent.attributes.find(
|
||||
(attr): attr is t.JSXAttribute =>
|
||||
t.isJSXAttribute(attr) && attr.name.name === "className"
|
||||
);
|
||||
|
||||
matchPath(valuePath, go => ({
|
||||
StringLiteral(path) {
|
||||
const { node } = path;
|
||||
const { value } = node;
|
||||
const trimmed = trim(value);
|
||||
if (trimmed.length) {
|
||||
const className = getClass(trimmed.join(" "));
|
||||
_.recordIfAbsent("css", {
|
||||
key: className,
|
||||
classNames: trimmed,
|
||||
location: _.sliceText(node),
|
||||
});
|
||||
path.replaceWith(_.getClassNameValue(className));
|
||||
}
|
||||
},
|
||||
ArrayExpression(path) {
|
||||
for (const element of path.get("elements")) {
|
||||
go(element);
|
||||
}
|
||||
},
|
||||
ObjectExpression(path) {
|
||||
const trimmed = evaluateArgs(path);
|
||||
const className = getClass(trimmed.join(" "));
|
||||
_.recordIfAbsent("css", {
|
||||
key: className,
|
||||
classNames: trimmed,
|
||||
location: _.sliceText(path.node),
|
||||
});
|
||||
path.replaceWith(_.getClassNameValue(className));
|
||||
},
|
||||
JSXExpressionContainer(path) {
|
||||
go(path.get("expression"));
|
||||
},
|
||||
ConditionalExpression(path) {
|
||||
go(path.get("consequent"));
|
||||
go(path.get("alternate"));
|
||||
},
|
||||
LogicalExpression(path) {
|
||||
go(path.get("right"));
|
||||
},
|
||||
CallExpression(path) {
|
||||
for (const arg of path.get("arguments")) {
|
||||
go(arg);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
const {
|
||||
identifier: id,
|
||||
jsxExpressionContainer: jsxBox,
|
||||
jsxIdentifier: jsxId,
|
||||
callExpression: call,
|
||||
} = t;
|
||||
|
||||
let valuePathNode = extractJSXContainer(valuePath.node);
|
||||
if (
|
||||
t.isArrayExpression(valuePathNode) &&
|
||||
valuePathNode.elements.every(node => t.isStringLiteral(node))
|
||||
) {
|
||||
valuePathNode = t.stringLiteral(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
(valuePathNode.elements as t.StringLiteral[]).map(node => node.value).join(" ")
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
const attrValue = classNameAttribute.value!;
|
||||
|
||||
// If both are string literals, we can merge them directly here
|
||||
if (t.isStringLiteral(attrValue) && t.isStringLiteral(valuePathNode)) {
|
||||
attrValue.value +=
|
||||
(attrValue.value.at(-1) === " " ? "" : " ") + valuePathNode.value;
|
||||
} else {
|
||||
const internal = extractJSXContainer(attrValue);
|
||||
if (
|
||||
t.isArrowFunctionExpression(internal) &&
|
||||
!t.isBlockStatement(internal.body)
|
||||
) {
|
||||
// className={({ isEntering }) => isEntering ? "enter" : "exit"}
|
||||
// className: ({ isEntering }) => _cx("${clsName}", isEntering ? "enter" : "exit")
|
||||
internal.body = wrap(internal.body);
|
||||
} else if (
|
||||
// if the existing className is already wrapped with cx, we unwrap it
|
||||
// to avoid double calling: cx(cx())
|
||||
t.isCallExpression(internal) &&
|
||||
t.isIdentifier(internal.callee) &&
|
||||
_.existingCx &&
|
||||
_.program.scope
|
||||
.getBinding(_.existingCx)
|
||||
?.referencePaths.map(p => p.node)
|
||||
.includes(internal.callee)
|
||||
) {
|
||||
classNameAttribute.value = jsxBox(
|
||||
call(_.getCx(path.scope), [
|
||||
valuePathNode,
|
||||
...(internal.arguments as (b.types.Expression | b.types.SpreadElement)[]),
|
||||
])
|
||||
);
|
||||
} else {
|
||||
classNameAttribute.value = jsxBox(wrap(internal));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const rest = parent.attributes.filter(attr => t.isJSXSpreadAttribute(attr));
|
||||
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)) {
|
||||
// props from argument and not modified anywhere, get the declaration of this argument
|
||||
const scope = path.scope.getBinding(arg.name);
|
||||
let index: number;
|
||||
// node is an identifier or object pattern in `params`
|
||||
// (props) => ... or ({ ...props }) => ...
|
||||
const node = scope?.path.node;
|
||||
if (
|
||||
scope &&
|
||||
!scope.constantViolations.length &&
|
||||
(t.isFunctionDeclaration(scope.path.parent) ||
|
||||
t.isArrowFunctionExpression(scope.path.parent)) &&
|
||||
(index = (scope.path.parent.params as t.Node[]).indexOf(node!)) !== -1 &&
|
||||
(t.isIdentifier(node) || t.isObjectPattern(node))
|
||||
) {
|
||||
const clsVar = path.scope.generateUidIdentifier("className");
|
||||
if (t.isIdentifier(node)) {
|
||||
// (props) => ...
|
||||
// ↪ ({ className, ...props }) => ...
|
||||
scope.path.parent.params[index] = t.objectPattern([
|
||||
t.objectProperty(id("className"), clsVar),
|
||||
t.restElement(node),
|
||||
]);
|
||||
} else {
|
||||
// ({ ...props }) => ...
|
||||
// ↪ ({ className, ...props }) => ...
|
||||
node.properties.unshift(t.objectProperty(id("className"), clsVar));
|
||||
}
|
||||
|
||||
parent.attributes.push(
|
||||
t.jsxAttribute(jsxId("className"), jsxBox(wrap(clsVar)))
|
||||
);
|
||||
} else {
|
||||
const tslibImport = _.getTSlibImport();
|
||||
rest[0].argument = call(t.memberExpression(tslibImport, id("__rest")), [
|
||||
arg,
|
||||
t.arrayExpression([t.stringLiteral("className")]),
|
||||
]);
|
||||
|
||||
parent.attributes.push(
|
||||
t.jsxAttribute(
|
||||
jsxId("className"),
|
||||
jsxBox(wrap(t.memberExpression(arg, id("className"))))
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Fallback
|
||||
const containerValue = t.isStringLiteral(valuePathNode)
|
||||
? valuePathNode
|
||||
: call(_.getCx(path.scope), [valuePathNode]);
|
||||
|
||||
parent.attributes.push(
|
||||
t.jsxAttribute(
|
||||
t.jsxIdentifier("className"),
|
||||
t.jsxExpressionContainer(containerValue)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (jsxAttributeAction === "delete") {
|
||||
path.remove();
|
||||
} else {
|
||||
path.node.value = copy!;
|
||||
if (Array.isArray(jsxAttributeAction) && jsxAttributeAction[0] === "rename") {
|
||||
// eslint-disable-next-line unicorn/consistent-destructuring
|
||||
path.node.name.name = jsxAttributeAction[1];
|
||||
}
|
||||
}
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
function getClsxImport(t: BabelTypes, cx: t.Identifier, clsx: string) {
|
||||
switch (clsx) {
|
||||
case "emotion":
|
||||
return t.importDeclaration(
|
||||
[t.importSpecifier(cx, t.identifier("cx"))],
|
||||
t.stringLiteral("@emotion/css")
|
||||
);
|
||||
case "clsx":
|
||||
return t.importDeclaration(
|
||||
[t.importSpecifier(cx, t.identifier("clsx"))],
|
||||
t.stringLiteral("clsx")
|
||||
);
|
||||
case "classnames":
|
||||
return t.importDeclaration(
|
||||
[t.importDefaultSpecifier(cx)],
|
||||
t.stringLiteral("classnames")
|
||||
);
|
||||
default:
|
||||
throw new Error("Unknown clsx library");
|
||||
}
|
||||
}
|
||||
|
||||
const definePlugin =
|
||||
<T>(fn: (runtime: typeof b) => b.Visitor<b.PluginPass & T>) =>
|
||||
(runtime: typeof b) => {
|
||||
const plugin: b.PluginObj<b.PluginPass & T> = {
|
||||
visitor: fn(runtime),
|
||||
};
|
||||
return plugin as b.PluginObj;
|
||||
};
|
||||
|
||||
const extractJSXContainer = (attr: NonNullable<t.JSXAttribute["value"]>): t.Expression =>
|
||||
attr.type === "JSXExpressionContainer" ? (attr.expression as t.Expression) : attr;
|
||||
|
||||
function matchPath(
|
||||
nodePath: NodePath<t.Node | null | undefined>,
|
||||
fns: (dig: (nodePath: NodePath<t.Node | null | undefined>) => void) => b.Visitor
|
||||
) {
|
||||
if (!nodePath.node) return;
|
||||
const fn = fns(path => matchPath(path, fns))[nodePath.node.type] as any;
|
||||
fn?.(nodePath);
|
||||
}
|
||||
|
||||
function getSuffix(add: boolean | undefined, entries: StyleMapEntry[]) {
|
||||
if (!add) return "";
|
||||
|
||||
const cacheKey = hash(entries.map(x => x.classNames).join(","));
|
||||
return `?${cacheKey}`;
|
||||
}
|
249
src/babel/macro.ts
Normal file
249
src/babel/macro.ts
Normal file
@ -0,0 +1,249 @@
|
||||
import type b from "@babel/core";
|
||||
import { type NodePath, type types as t } from "@babel/core";
|
||||
import invariant from "tiny-invariant";
|
||||
|
||||
import type { SupportedTag } from "../macro";
|
||||
import { macroNames } from "../shared";
|
||||
|
||||
import { evaluateArgs, trimPrefix } from "./utils";
|
||||
|
||||
import type { BabelPluginUtils } from "./index";
|
||||
|
||||
type BabelTypes = typeof b.types;
|
||||
|
||||
export function handleMacro({
|
||||
t,
|
||||
path,
|
||||
_,
|
||||
}: {
|
||||
t: BabelTypes;
|
||||
path: NodePath<t.Program>;
|
||||
_: BabelPluginUtils;
|
||||
}) {
|
||||
const macros = getMacros(t, path, macroNames).map(macro => mapMacro(t, macro));
|
||||
|
||||
for (const { callee, imported, prefix } of macros) {
|
||||
const type = imported === "tw" ? "css" : imported === "tws" ? "js" : undefined;
|
||||
if (!type) continue;
|
||||
|
||||
if (isNodePath(callee, t.isTaggedTemplateExpression)) {
|
||||
const { node } = callee;
|
||||
const { quasi } = node;
|
||||
|
||||
invariant(!quasi.expressions.length, `Macro call should not contain expressions`);
|
||||
|
||||
const value = quasi.quasis[0].value.cooked;
|
||||
if (value) {
|
||||
if (prefix && supportedTags.has(prefix as SupportedTag) && type === "css") {
|
||||
const list = trimPrefix(value);
|
||||
const className = _.getClass(type, list.join(" "));
|
||||
_.recordIfAbsent(type, {
|
||||
key: className,
|
||||
classNames: list,
|
||||
location: _.sliceText(node),
|
||||
});
|
||||
callee.replaceWith(
|
||||
t.callExpression(_.getClassedImport(), [
|
||||
t.stringLiteral(prefix),
|
||||
t.stringLiteral(className),
|
||||
])
|
||||
);
|
||||
} else {
|
||||
const list = trimPrefix(value, prefix ? prefix + ":" : undefined);
|
||||
const className = _.getClass(type, list.join(" "));
|
||||
_.recordIfAbsent(type, {
|
||||
key: className,
|
||||
classNames: list,
|
||||
location: _.sliceText(node),
|
||||
});
|
||||
_.replaceWithImport({
|
||||
type,
|
||||
path: callee,
|
||||
className: addIf(className, addGroup(list)),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (isNodePath(callee, t.isCallExpression)) {
|
||||
const { node } = callee;
|
||||
if (!t.isIdentifier(node.callee)) continue;
|
||||
|
||||
const list = callee.get("arguments").flatMap(evaluateArgs);
|
||||
const className = _.getClass("css", list.join(" "));
|
||||
_.recordIfAbsent(type, {
|
||||
key: className,
|
||||
classNames: list,
|
||||
location: _.sliceText(node),
|
||||
});
|
||||
_.replaceWithImport({
|
||||
type,
|
||||
path: callee,
|
||||
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) {
|
||||
return suffix ? text + suffix : text;
|
||||
}
|
||||
|
||||
function mapMacro(t: BabelTypes, macro: ReturnType<typeof getMacros>[number]) {
|
||||
let callee = macro.local.parentPath;
|
||||
const prefix: string[] = [];
|
||||
|
||||
while (isNodePath(callee, t.isMemberExpression)) {
|
||||
invariant(t.isIdentifier(callee.node.property), "Invalid member expression");
|
||||
prefix.unshift(
|
||||
callee.node.property.name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()
|
||||
);
|
||||
callee = callee.parentPath;
|
||||
}
|
||||
|
||||
return {
|
||||
callee,
|
||||
imported: macro.imported,
|
||||
prefix: prefix.length ? prefix.join(":") : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function getName(t: BabelTypes, exp: t.Node) {
|
||||
if (t.isIdentifier(exp)) {
|
||||
return exp.name;
|
||||
} else if (t.isStringLiteral(exp)) {
|
||||
return exp.value;
|
||||
}
|
||||
}
|
||||
|
||||
function getMacros(
|
||||
t: BabelTypes,
|
||||
programPath: NodePath<t.Program>,
|
||||
importSources: string[]
|
||||
) {
|
||||
const importDecs = programPath
|
||||
.get("body")
|
||||
.filter(x => isNodePath(x, t.isImportDeclaration))
|
||||
.filter(x => importSources.includes(x.node.source.value));
|
||||
|
||||
const macros = importDecs
|
||||
.flatMap(x => x.get("specifiers"))
|
||||
.map(x => {
|
||||
const local = x.get("local");
|
||||
if (isNodePath(x, t.isImportNamespaceSpecifier)) {
|
||||
return local.scope
|
||||
.getOwnBinding(local.node.name)!
|
||||
.referencePaths.map(p => p.parentPath)
|
||||
.filter(p => isNodePath(p, t.isMemberExpression))
|
||||
.map(p => ({
|
||||
local: p,
|
||||
imported: getName(t, p.node.property)!,
|
||||
}))
|
||||
.filter(p => p.imported);
|
||||
} else if (t.isImportSpecifier(x.node)) {
|
||||
const imported = x.node.imported;
|
||||
return local.scope.getOwnBinding(local.node.name)!.referencePaths.map(p => ({
|
||||
local: p as NodePath<t.Identifier>,
|
||||
imported: getName(t, imported)!,
|
||||
}));
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
.flat(1);
|
||||
|
||||
for (const x of importDecs) {
|
||||
x.remove();
|
||||
}
|
||||
|
||||
return macros;
|
||||
}
|
||||
|
||||
const isNodePath = <T extends t.Node>(
|
||||
nodePath: NodePath<t.Node | null | undefined> | null,
|
||||
predicate: (node: t.Node) => node is T
|
||||
): nodePath is NodePath<T> => Boolean(nodePath?.node && predicate(nodePath.node));
|
||||
|
||||
const supportedTags = new Set<SupportedTag>([
|
||||
"a",
|
||||
"abbr",
|
||||
"address",
|
||||
"article",
|
||||
"b",
|
||||
"bdi",
|
||||
"blockquote",
|
||||
"button",
|
||||
"caption",
|
||||
"cite",
|
||||
"code",
|
||||
"col",
|
||||
"colgroup",
|
||||
"dd",
|
||||
"del",
|
||||
"details",
|
||||
"dialog",
|
||||
"div",
|
||||
"dl",
|
||||
"dt",
|
||||
"em",
|
||||
"fieldset",
|
||||
"figcaption",
|
||||
"figure",
|
||||
"footer",
|
||||
"form",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"header",
|
||||
"hr",
|
||||
"i",
|
||||
"iframe",
|
||||
"img",
|
||||
"input",
|
||||
"ins",
|
||||
"kbd",
|
||||
"label",
|
||||
"legend",
|
||||
"li",
|
||||
"main",
|
||||
"mark",
|
||||
"menu",
|
||||
"meter",
|
||||
"nav",
|
||||
"ol",
|
||||
"output",
|
||||
"p",
|
||||
"picture",
|
||||
"pre",
|
||||
"progress",
|
||||
"q",
|
||||
"rt",
|
||||
"ruby",
|
||||
"section",
|
||||
"select",
|
||||
"span",
|
||||
"strong",
|
||||
"sub",
|
||||
"summary",
|
||||
"sup",
|
||||
"svg",
|
||||
"table",
|
||||
"tbody",
|
||||
"td",
|
||||
"textarea",
|
||||
"tfoot",
|
||||
"th",
|
||||
"thead",
|
||||
"time",
|
||||
"tr",
|
||||
"u",
|
||||
"ul",
|
||||
"var",
|
||||
"video",
|
||||
"wbr",
|
||||
]);
|
45
src/babel/utils.ts
Normal file
45
src/babel/utils.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import type { NodePath } from "@babel/core";
|
||||
import { isPlainObject } from "lodash-es";
|
||||
import invariant from "tiny-invariant";
|
||||
|
||||
export function evaluateArgs(path: NodePath) {
|
||||
const { confident, value } = path.evaluate();
|
||||
invariant(confident, "Argument cannot be statically evaluated");
|
||||
|
||||
if (typeof value === "string") {
|
||||
return trim(value);
|
||||
}
|
||||
|
||||
if (isPlainObject(value)) {
|
||||
return flatMapEntries(value, (classes, modifier) => {
|
||||
if (modifier === "data" && isPlainObject(classes)) {
|
||||
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}]:`)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
invariant(
|
||||
typeof classes === "string",
|
||||
`Value for "${modifier}" should be a string`
|
||||
);
|
||||
return trimPrefix(classes, modifier + ":");
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error("Invalid argument type");
|
||||
}
|
||||
|
||||
export const trim = (value: string) =>
|
||||
value.replace(/\s+/g, " ").trim().split(" ").filter(Boolean);
|
||||
|
||||
export const trimPrefix = (cls: string, prefix = "") =>
|
||||
trim(cls).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));
|
@ -1,5 +1,7 @@
|
||||
// MIT License. Copyright (c) 2017 Brice BERNARD
|
||||
// https://github.com/brikou/CSS-in-JS-generator/commit/2a887d0d96f1d5044039d0e0457001f0fde0def0
|
||||
import JSON5 from "json5";
|
||||
import { camelCase } from "lodash-es";
|
||||
import {
|
||||
type AtRule,
|
||||
type Builder,
|
||||
@ -8,11 +10,9 @@ import {
|
||||
type Rule,
|
||||
parse,
|
||||
} from "postcss";
|
||||
import JSON5 from "json5";
|
||||
import parseSelector from "postcss-selector-parser";
|
||||
import Stringifier from "postcss/lib/stringifier";
|
||||
import parseSelector from "postcss-selector-parser";
|
||||
import { type Element, compile } from "stylis";
|
||||
import { camelCase } from "lodash";
|
||||
|
||||
function getSelectorScope(selector: string): string {
|
||||
let selectorScope = "root";
|
||||
@ -129,7 +129,8 @@ const convertScopeToModuleName = (scope: string) =>
|
||||
"_$1"
|
||||
);
|
||||
|
||||
export function convertCssToJS(
|
||||
/** @internal */
|
||||
export function toJSCode(
|
||||
css: string,
|
||||
mapClassNames: (className: string) => string = convertScopeToModuleName
|
||||
): string {
|
||||
@ -211,7 +212,7 @@ export function convertCssToJS(
|
||||
? `injectGlobal\`${style}\n\`;\n`
|
||||
: `\nexport const ${mapClassNames(
|
||||
scope
|
||||
)} = ${JSON5.stringify(asJSObject(style), null, 2)};\n`;
|
||||
)} = ${JSON5.stringify(cssToJS(style), null, 2)};\n`;
|
||||
}
|
||||
|
||||
return res.trim();
|
||||
@ -299,7 +300,7 @@ function simplifyValue(propName: string, value: string) {
|
||||
return value;
|
||||
}
|
||||
|
||||
function asJSObject(inputCssText: string) {
|
||||
export function cssToJS(inputCssText: string): Record<string, any> {
|
||||
const css = compile(inputCssText);
|
||||
const result: Record<string, string> = {};
|
||||
|
||||
@ -338,5 +339,5 @@ function asJSObject(inputCssText: string) {
|
||||
for (const node of css) {
|
||||
walk(result, node);
|
||||
}
|
||||
return result as React.CSSProperties;
|
||||
return result;
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { extname } from "node:path";
|
||||
import { once } from "lodash";
|
||||
|
||||
import type babel from "@babel/core";
|
||||
import type * as esbuild from "esbuild";
|
||||
import { transformSync } from "@babel/core";
|
||||
import type * as esbuild from "esbuild";
|
||||
import { once } from "lodash-es";
|
||||
|
||||
/**
|
||||
* An esbuild plugin that processes files with Babel if `plugins` is not empty.
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
import type * as esbuild from "esbuild";
|
||||
import { CssSyntaxError } from "postcss";
|
||||
|
||||
import { type Compile, type StyleMap, type StyleMapEntry, pkgName } from "./shared";
|
||||
|
||||
import type { BuildStyleFile } from "./index";
|
||||
|
||||
const PLUGIN_NAME = "tailwind";
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getClassName } from "./index";
|
||||
|
||||
import { getBuild } from "./__tests__/utils";
|
||||
|
||||
import { getClassName } from "./index";
|
||||
|
||||
describe("babel-tailwind", () => {
|
||||
const compileESBuild = getBuild("main");
|
||||
|
||||
|
120
src/index.ts
120
src/index.ts
@ -1,14 +1,18 @@
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
import hash from "@emotion/hash";
|
||||
import type { BabelOptions, Options as ReactOptions } from "@vitejs/plugin-react";
|
||||
import { transformSync } from "esbuild";
|
||||
import { memoize, without } from "lodash-es";
|
||||
import type { Config } from "tailwindcss";
|
||||
import type { SetRequired } from "type-fest";
|
||||
import { transformSync } from "esbuild";
|
||||
import type postcss from "postcss";
|
||||
import { memoize, without } from "lodash";
|
||||
import { type ClassNameCollector, babelTailwind } from "./babel-tailwind";
|
||||
import type { PluginOption } from "vite";
|
||||
|
||||
import { type ClassNameCollector, babelTailwind } from "./babel/index";
|
||||
import { toJSCode } from "./css-to-js";
|
||||
import { esbuildPlugin } from "./esbuild-postcss";
|
||||
import { vitePlugin } from "./vite-plugin";
|
||||
import { type StyleMap, createPostCSS } from "./shared";
|
||||
import { convertCssToJS } from "./css-to-js";
|
||||
import { vitePlugin } from "./vite-plugin";
|
||||
|
||||
export { isMacrosName } from "./vite-plugin";
|
||||
|
||||
@ -18,7 +22,9 @@ export { createPostCSS } from "./shared";
|
||||
type GetClassName = (className: string) => string;
|
||||
export type BuildStyleFile = (
|
||||
path: string
|
||||
) => Promise<readonly ["css", string] | readonly ["js", string]>;
|
||||
) => Promise<readonly ["css" | "local-css", string] | readonly ["js", string]>;
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
export interface TailwindPluginOptions {
|
||||
/**
|
||||
@ -36,11 +42,6 @@ export interface TailwindPluginOptions {
|
||||
*/
|
||||
prefix?: string;
|
||||
|
||||
/**
|
||||
* Additional PostCSS plugins (optional)
|
||||
*/
|
||||
postCSSPlugins?: postcss.AcceptedPlugin[];
|
||||
|
||||
/**
|
||||
* Attribute to use for tailwind classes in JSX
|
||||
* @default "css"
|
||||
@ -54,7 +55,7 @@ export interface TailwindPluginOptions {
|
||||
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)}`
|
||||
*/
|
||||
getClassName?: GetClassName;
|
||||
@ -64,6 +65,11 @@ export interface TailwindPluginOptions {
|
||||
*/
|
||||
clsx: "clsx" | "classnames" | "emotion";
|
||||
|
||||
/**
|
||||
* Use react-aria-component’s `composeRenderProps` function.
|
||||
*/
|
||||
composeRenderProps?: boolean;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@ -85,16 +91,22 @@ export interface TailwindPluginOptions {
|
||||
* Keep the original classnames in the CSS output
|
||||
*/
|
||||
addSourceAsComment?: boolean;
|
||||
|
||||
/**
|
||||
* Emit as CSS modules
|
||||
*/
|
||||
cssModules?: boolean;
|
||||
|
||||
/**
|
||||
* Emit type. `css-import` for plain CSS import,
|
||||
* `css-module` for CSS modules, `css-in-js` for JS.
|
||||
*/
|
||||
// emitType: "css-import" | "css-module" | "css-in-js";
|
||||
}
|
||||
|
||||
export type ResolveTailwindOptions = SetRequired<
|
||||
TailwindPluginOptions,
|
||||
| "clsx"
|
||||
| "jsxAttributeAction"
|
||||
| "jsxAttributeName"
|
||||
| "postCSSPlugins"
|
||||
| "styleMap"
|
||||
| "tailwindConfig"
|
||||
"clsx" | "jsxAttributeAction" | "jsxAttributeName" | "styleMap" | "tailwindConfig"
|
||||
>;
|
||||
|
||||
/**
|
||||
@ -103,6 +115,15 @@ export type ResolveTailwindOptions = SetRequired<
|
||||
*/
|
||||
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
|
||||
* classNames in JS.
|
||||
@ -120,12 +141,11 @@ export const getClassName: GetClassName = cls => "tw-" + hash(cls);
|
||||
* });
|
||||
*/
|
||||
export function getTailwindPlugins(options: TailwindPluginOptions) {
|
||||
const { addSourceAsComment } = options;
|
||||
const { addSourceAsComment, compile: _compile, cssModules } = options;
|
||||
const resolvedOptions: ResolveTailwindOptions = {
|
||||
getClassName,
|
||||
jsxAttributeAction: "delete",
|
||||
jsxAttributeName: "css",
|
||||
postCSSPlugins: [],
|
||||
styleMap: new Map(),
|
||||
tailwindConfig: {},
|
||||
...options,
|
||||
@ -134,14 +154,19 @@ export function getTailwindPlugins(options: TailwindPluginOptions) {
|
||||
const getCompiler = () => createPostCSS(resolvedOptions);
|
||||
|
||||
const { styleMap } = resolvedOptions;
|
||||
const compile = options.compile ?? memoize(getCompiler());
|
||||
const compile = _compile ?? memoize(getCompiler());
|
||||
|
||||
const buildStyleFile: BuildStyleFile = async path => {
|
||||
const styles = styleMap.get(path)!;
|
||||
const compiled = await compile(
|
||||
styles
|
||||
.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 [
|
||||
`.${key} {`,
|
||||
addSourceAsComment && ` /* @preserve ${tw} */`,
|
||||
@ -154,24 +179,65 @@ export function getTailwindPlugins(options: TailwindPluginOptions) {
|
||||
.join("\n")
|
||||
);
|
||||
if (path.endsWith(".css")) {
|
||||
return ["css", transformSync(compiled, { loader: "css" }).code] as const;
|
||||
return [
|
||||
cssModules ? "local-css" : "css",
|
||||
transformSync(compiled, { loader: "css" }).code,
|
||||
] as const;
|
||||
} else if (path.endsWith(".js")) {
|
||||
const js = convertCssToJS(compiled, x => x.slice(1));
|
||||
const js = toJSCode(compiled, x => x.slice(1));
|
||||
return ["js", js] as const;
|
||||
} else {
|
||||
throw new Error("Unknown file extension");
|
||||
}
|
||||
};
|
||||
|
||||
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 {
|
||||
compile,
|
||||
babel: (onCollect?: ClassNameCollector) => babelTailwind(resolvedOptions, onCollect),
|
||||
esbuild: () => esbuildPlugin({ styleMap, compile, buildStyleFile }),
|
||||
/** Requires `options.vite` to be `true`. */
|
||||
vite: () => vitePlugin({ styleMap, compile, buildStyleFile }),
|
||||
vite: () => {
|
||||
resolvedOptions.vite = true;
|
||||
return vitePlugin({ styleMap, compile, buildStyleFile });
|
||||
},
|
||||
react,
|
||||
styleMap,
|
||||
options,
|
||||
getCompiler,
|
||||
buildStyleFile,
|
||||
[Symbol.dispose]() {
|
||||
styleMap.clear();
|
||||
},
|
||||
|
87
src/macro.d.ts
vendored
87
src/macro.d.ts
vendored
@ -10,6 +10,87 @@ interface RecursiveStringObject {
|
||||
|
||||
type CSSAttributeValue = string | (string | RecursiveStringObject)[];
|
||||
|
||||
export type SupportedTag =
|
||||
| "a"
|
||||
| "abbr"
|
||||
| "address"
|
||||
| "article"
|
||||
| "b"
|
||||
| "bdi"
|
||||
| "blockquote"
|
||||
| "button"
|
||||
| "caption"
|
||||
| "cite"
|
||||
| "code"
|
||||
| "col"
|
||||
| "colgroup"
|
||||
| "dd"
|
||||
| "del"
|
||||
| "details"
|
||||
| "dialog"
|
||||
| "div"
|
||||
| "dl"
|
||||
| "dt"
|
||||
| "em"
|
||||
| "fieldset"
|
||||
| "figcaption"
|
||||
| "figure"
|
||||
| "footer"
|
||||
| "form"
|
||||
| "h1"
|
||||
| "h2"
|
||||
| "h3"
|
||||
| "h4"
|
||||
| "h5"
|
||||
| "h6"
|
||||
| "header"
|
||||
| "hr"
|
||||
| "i"
|
||||
| "iframe"
|
||||
| "img"
|
||||
| "input"
|
||||
| "ins"
|
||||
| "kbd"
|
||||
| "label"
|
||||
| "legend"
|
||||
| "li"
|
||||
| "main"
|
||||
| "mark"
|
||||
| "menu"
|
||||
| "meter"
|
||||
| "nav"
|
||||
| "ol"
|
||||
| "output"
|
||||
| "p"
|
||||
| "picture"
|
||||
| "pre"
|
||||
| "progress"
|
||||
| "q"
|
||||
| "rt"
|
||||
| "ruby"
|
||||
| "section"
|
||||
| "select"
|
||||
| "span"
|
||||
| "strong"
|
||||
| "sub"
|
||||
| "summary"
|
||||
| "sup"
|
||||
| "svg"
|
||||
| "table"
|
||||
| "tbody"
|
||||
| "td"
|
||||
| "textarea"
|
||||
| "tfoot"
|
||||
| "th"
|
||||
| "thead"
|
||||
| "time"
|
||||
| "tr"
|
||||
| "u"
|
||||
| "ul"
|
||||
| "var"
|
||||
| "video"
|
||||
| "wbr";
|
||||
|
||||
type Modifier =
|
||||
| "2xl"
|
||||
| "active"
|
||||
@ -69,6 +150,10 @@ export type TailwindFunction = {
|
||||
(...args: (string | RecursiveStringObject)[]): string;
|
||||
} & {
|
||||
[key in Modifier]: TailwindFunction;
|
||||
} & {
|
||||
[tag in SupportedTag]: (
|
||||
strings: TemplateStringsArray
|
||||
) => React.FunctionComponent<React.JSX.IntrinsicElements[tag]>;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -99,5 +184,5 @@ type TailwindStyleFunctionReturn = Config extends { tws: infer T } ? T : never;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type
|
||||
export interface Config {}
|
||||
|
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>;
|
@ -1,9 +1,13 @@
|
||||
import tailwind from "tailwindcss";
|
||||
import postcss from "postcss";
|
||||
import tailwind from "tailwindcss";
|
||||
|
||||
import type { ResolveTailwindOptions } from "./index";
|
||||
|
||||
export const { name: pkgName } = [require][0]("../package.json");
|
||||
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;
|
||||
@ -29,15 +33,13 @@ export type StyleMap = Map</* filename */ string, StyleMapEntry[]>;
|
||||
|
||||
export function createPostCSS({
|
||||
tailwindConfig,
|
||||
postCSSPlugins,
|
||||
prefix,
|
||||
}: Pick<ResolveTailwindOptions, "tailwindConfig" | "postCSSPlugins" | "prefix">) {
|
||||
}: Pick<ResolveTailwindOptions, "tailwindConfig" | "prefix">) {
|
||||
const post = postcss([
|
||||
tailwind({
|
||||
...tailwindConfig,
|
||||
content: [{ raw: "<br>", extension: "html" }],
|
||||
}),
|
||||
...postCSSPlugins,
|
||||
]);
|
||||
|
||||
return async (css: string) => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { type FunctionComponent, forwardRef } from "react";
|
||||
import cx from "clsx";
|
||||
import { type FunctionComponent, forwardRef } from "react";
|
||||
|
||||
export interface WithClassName<Props = object> extends FunctionComponent<Props> {
|
||||
className: string;
|
||||
@ -23,11 +23,11 @@ export const classed: {
|
||||
className: PresetClassName<InputProps>,
|
||||
defaultProps?: Partial<InputProps>
|
||||
): React.FunctionComponent<InputProps>;
|
||||
<K extends keyof JSX.IntrinsicElements>(
|
||||
<K extends keyof React.JSX.IntrinsicElements>(
|
||||
type: K,
|
||||
className: PresetClassName<JSX.IntrinsicElements[K]>,
|
||||
defaultProps?: Partial<JSX.IntrinsicElements[K]>
|
||||
): React.FunctionComponent<JSX.IntrinsicElements[K]>;
|
||||
className: PresetClassName<React.JSX.IntrinsicElements[K]>,
|
||||
defaultProps?: Partial<React.JSX.IntrinsicElements[K]>
|
||||
): React.FunctionComponent<React.JSX.IntrinsicElements[K]>;
|
||||
(
|
||||
type: string,
|
||||
className: PresetClassName,
|
||||
@ -67,3 +67,10 @@ export const classed: {
|
||||
component.className = className;
|
||||
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);
|
||||
}
|
15
src/vendor/animate.ts
vendored
15
src/vendor/animate.ts
vendored
@ -1,5 +1,6 @@
|
||||
// https://github.com/jamiebuilds/tailwindcss-animate/commit/ac0dd3a3c81681b78f1d8ea5e7478044213995e1
|
||||
import plugin from "tailwindcss/plugin";
|
||||
// https://github.com/tailwindlabs/tailwindcss/discussions/11164#discussioncomment-5819097
|
||||
import plugin from "tailwindcss/plugin.js";
|
||||
import type { PluginAPI } from "tailwindcss/types/config";
|
||||
|
||||
function filterDefault<T extends object>(values: T) {
|
||||
@ -11,11 +12,7 @@ function filterDefault<T extends object>(values: T) {
|
||||
export default plugin(
|
||||
({ addUtilities, matchUtilities, theme }) => {
|
||||
addUtilities({
|
||||
"@keyframes enter": theme("keyframes.enter"),
|
||||
"@keyframes exit": theme("keyframes.exit"),
|
||||
".animate-in": {
|
||||
animationName: "enter",
|
||||
animationDuration: theme("animationDuration.DEFAULT"),
|
||||
"--tw-enter-opacity": "initial",
|
||||
"--tw-enter-scale": "initial",
|
||||
"--tw-enter-rotate": "initial",
|
||||
@ -23,8 +20,6 @@ export default plugin(
|
||||
"--tw-enter-translate-y": "initial",
|
||||
},
|
||||
".animate-out": {
|
||||
animationName: "exit",
|
||||
animationDuration: theme("animationDuration.DEFAULT"),
|
||||
"--tw-exit-opacity": "initial",
|
||||
"--tw-exit-scale": "initial",
|
||||
"--tw-exit-rotate": "initial",
|
||||
@ -168,6 +163,10 @@ export default plugin(
|
||||
1: "1",
|
||||
infinite: "infinite",
|
||||
},
|
||||
animation: ({ theme }) => ({
|
||||
out: `leave ${theme("animationDuration.DEFAULT")}`,
|
||||
in: `enter ${theme("animationDuration.DEFAULT")}`,
|
||||
}),
|
||||
keyframes: {
|
||||
enter: {
|
||||
from: {
|
||||
@ -176,7 +175,7 @@ export default plugin(
|
||||
"translate3d(var(--tw-enter-translate-x, 0), var(--tw-enter-translate-y, 0), 0) scale3d(var(--tw-enter-scale, 1), var(--tw-enter-scale, 1), var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))",
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
leave: {
|
||||
to: {
|
||||
opacity: "var(--tw-exit-opacity, 1)",
|
||||
transform:
|
||||
|
2
src/vendor/aspect-ratio.ts
vendored
2
src/vendor/aspect-ratio.ts
vendored
@ -1,5 +1,5 @@
|
||||
// https://github.com/tailwindlabs/tailwindcss-aspect-ratio/commit/b2a9d02229946f3430c0013198be2affa7a175da
|
||||
import plugin from "tailwindcss/plugin";
|
||||
import plugin from "tailwindcss/plugin.js";
|
||||
|
||||
const baseStyles = {
|
||||
position: "relative",
|
||||
|
4
src/vendor/container-queries.ts
vendored
4
src/vendor/container-queries.ts
vendored
@ -1,5 +1,5 @@
|
||||
// https://github.com/tailwindlabs/tailwindcss-container-queries/commit/f8d4307afdd3d913c3ddd406334c1a07f427c5b3
|
||||
import plugin from "tailwindcss/plugin";
|
||||
// https://github.com/tailwindlabs/tailwindcss-container-queries/commit/ef92eba7a7df60659da1ab8dd584346f00efae73
|
||||
import plugin from "tailwindcss/plugin.js";
|
||||
|
||||
function parseValue(value: string) {
|
||||
const numericValue = value.match(/^(\d+\.\d+|\d+|\.\d+)\D+/)?.[1] ?? null;
|
||||
|
18
src/vendor/forms.ts
vendored
18
src/vendor/forms.ts
vendored
@ -1,7 +1,7 @@
|
||||
// https://github.com/tailwindlabs/tailwindcss-forms/commit/c9d9da3e010b194a1f0e9c36fbd98c83e4762840
|
||||
import plugin from "tailwindcss/plugin";
|
||||
import defaultTheme from "tailwindcss/defaultTheme";
|
||||
import colors from "tailwindcss/colors";
|
||||
import colors from "tailwindcss/colors.js";
|
||||
import defaultTheme from "tailwindcss/defaultTheme.js";
|
||||
import plugin from "tailwindcss/plugin.js";
|
||||
import type { CSSRuleObject } from "tailwindcss/types/config";
|
||||
|
||||
const shorterNames = {
|
||||
@ -125,6 +125,16 @@ type Strategy = "base" | "class";
|
||||
export default plugin.withOptions<{ strategy?: Strategy }>(
|
||||
options =>
|
||||
function ({ addBase, addComponents, theme }) {
|
||||
function resolveChevronColor(color: string, fallback: string) {
|
||||
const resolved = theme(color);
|
||||
|
||||
if (!resolved || resolved.includes("var(")) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return resolved.replace("<alpha-value>", "1");
|
||||
}
|
||||
|
||||
const strategy =
|
||||
options?.strategy === undefined ? ["base", "class"] : [options.strategy];
|
||||
|
||||
@ -259,7 +269,7 @@ export default plugin.withOptions<{ strategy?: Strategy }>(
|
||||
class: [".form-select"],
|
||||
styles: {
|
||||
"background-image": `url("${svgToDataUri(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20"><path stroke="${theme(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20"><path stroke="${resolveChevronColor(
|
||||
"colors.gray.500",
|
||||
colors.gray[500]
|
||||
)}" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6 8l4 4 4-4"/></svg>`
|
||||
|
158
src/vendor/react-aria-components-4.ts
vendored
Normal file
158
src/vendor/react-aria-components-4.ts
vendored
Normal file
@ -0,0 +1,158 @@
|
||||
// https://github.com/adobe/react-spectrum/blob/41ef71d18049af1fcec7d4a9953c2600ed1fa116/packages/tailwindcss-react-aria-components/src/index.js
|
||||
import plugin from "tailwindcss/plugin.js";
|
||||
import type { PluginAPI } from "tailwindcss/types/config";
|
||||
|
||||
// Order of these is important because it determines which states win in a conflict.
|
||||
// We mostly follow Tailwind's defaults, adding our additional states following the categories they define.
|
||||
// https://github.com/tailwindlabs/tailwindcss/blob/304c2bad6cb5fcb62754a4580b1c8f4c16b946ea/src/corePlugins.js#L83
|
||||
const attributes = {
|
||||
boolean: [
|
||||
// Conditions
|
||||
"allows-removing",
|
||||
"allows-sorting",
|
||||
"allows-dragging",
|
||||
"has-submenu",
|
||||
|
||||
// States
|
||||
"open",
|
||||
"expanded",
|
||||
"entering",
|
||||
"exiting",
|
||||
"indeterminate",
|
||||
["placeholder-shown", "placeholder"],
|
||||
"current",
|
||||
"required",
|
||||
"unavailable",
|
||||
"invalid",
|
||||
["read-only", "readonly"],
|
||||
"outside-month",
|
||||
"outside-visible-range",
|
||||
"pending",
|
||||
|
||||
// Content
|
||||
"empty",
|
||||
|
||||
// Interactive states
|
||||
"focus-within",
|
||||
["hover", "hovered"],
|
||||
["focus", "focused"],
|
||||
"focus-visible",
|
||||
"pressed",
|
||||
"selected",
|
||||
"selection-start",
|
||||
"selection-end",
|
||||
"dragging",
|
||||
"drop-target",
|
||||
"resizing",
|
||||
"disabled",
|
||||
] as const,
|
||||
enum: {
|
||||
placement: ["left", "right", "top", "bottom"],
|
||||
type: ["literal", "year", "month", "day"],
|
||||
layout: ["grid", "stack"],
|
||||
orientation: ["horizontal", "vertical"],
|
||||
"selection-mode": ["single", "multiple"],
|
||||
"resizable-direction": ["right", "left", "both"],
|
||||
"sort-direction": ["ascending", "descending"],
|
||||
},
|
||||
};
|
||||
|
||||
const shortNames: Record<string, string> = {
|
||||
"selection-mode": "selection",
|
||||
"resizable-direction": "resizable",
|
||||
"sort-direction": "sort",
|
||||
};
|
||||
|
||||
// Variants we use that are already defined by Tailwind:
|
||||
// https://github.com/tailwindlabs/tailwindcss/blob/a2fa6932767ab328515f743d6188c2164ad2a5de/src/corePlugins.js#L84
|
||||
const nativeVariants = [
|
||||
"indeterminate",
|
||||
"required",
|
||||
"invalid",
|
||||
"empty",
|
||||
"focus-visible",
|
||||
"focus-within",
|
||||
"disabled",
|
||||
];
|
||||
const nativeVariantSelectors = new Map<string, string>([
|
||||
...nativeVariants.map(variant => [variant, `:${variant}`] as const),
|
||||
["hovered", ":hover"],
|
||||
["focused", ":focus"],
|
||||
["readonly", ":read-only"],
|
||||
["open", "[open]"],
|
||||
["expanded", "[expanded]"],
|
||||
]);
|
||||
|
||||
// Variants where both native and RAC attributes should apply. We don't override these.
|
||||
const nativeMergeSelectors = new Map([["placeholder", ":placeholder-shown"]]);
|
||||
|
||||
type SelectorFn = (wrap: (s: string) => string) => string;
|
||||
type SelectorValue = string | SelectorFn;
|
||||
type Selector = string | [string, SelectorValue];
|
||||
|
||||
// If no prefix is specified, we want to avoid overriding native variants on non-RAC components, so we only target elements with the data-rac attribute for those variants.
|
||||
function getSelector(
|
||||
prefix: string,
|
||||
attributeName: string,
|
||||
attributeValue: string | null
|
||||
): Selector {
|
||||
const baseSelector = attributeValue
|
||||
? `[data-${attributeName}="${attributeValue}"]`
|
||||
: `[data-${attributeName}]`;
|
||||
const nativeSelector = nativeVariantSelectors.get(attributeName);
|
||||
if (prefix === "" && nativeSelector) {
|
||||
const wrappedNativeSelector = `&:not([data-rac])${nativeSelector}`;
|
||||
let nativeSelectorGenerator: SelectorValue = wrappedNativeSelector;
|
||||
if (nativeSelector === ":hover") {
|
||||
nativeSelectorGenerator = wrap =>
|
||||
`@media (hover: hover) { ${wrap(wrappedNativeSelector)} }`;
|
||||
}
|
||||
return [`&[data-rac]${baseSelector}`, nativeSelectorGenerator];
|
||||
} else if (prefix === "" && nativeMergeSelectors.has(attributeName)) {
|
||||
return [`&${baseSelector}`, `&${nativeMergeSelectors.get(attributeName)}`];
|
||||
} else {
|
||||
return `&${baseSelector}`;
|
||||
}
|
||||
}
|
||||
|
||||
const mapSelector = (selector: Selector, fn: (v: SelectorValue) => string) =>
|
||||
Array.isArray(selector) ? selector.map(fn) : fn(selector);
|
||||
|
||||
const wrapSelector = (selector: SelectorValue, wrap: (text: string) => string) =>
|
||||
typeof selector === "function" ? selector(wrap) : wrap(selector);
|
||||
|
||||
const addVariants = (
|
||||
variantName: string,
|
||||
selectors: Selector,
|
||||
addVariant: PluginAPI["addVariant"]
|
||||
) => {
|
||||
addVariant(
|
||||
variantName,
|
||||
mapSelector(selectors, selector => wrapSelector(selector, s => s))
|
||||
);
|
||||
};
|
||||
|
||||
export default plugin.withOptions<{ prefix: string }>(options => ({ addVariant }) => {
|
||||
const prefix = options?.prefix ? `${options.prefix}-` : "";
|
||||
|
||||
// Enum attributes go first because currently they are all non-interactive states.
|
||||
for (const [attributeName, value] of Object.entries(attributes.enum) as [
|
||||
keyof typeof attributes.enum,
|
||||
string[],
|
||||
][]) {
|
||||
for (const [i, attributeValue] of value.entries()) {
|
||||
const name = shortNames[attributeName] || attributeName;
|
||||
const variantName = `${prefix}${name}-${attributeValue}`;
|
||||
const selectors = getSelector(prefix, attributeName, attributeValue);
|
||||
addVariants(variantName, selectors, addVariant, i);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [i, attribute] of attributes.boolean.entries()) {
|
||||
let variantName = Array.isArray(attribute) ? attribute[0] : attribute;
|
||||
variantName = `${prefix}${variantName}`;
|
||||
const attributeName = Array.isArray(attribute) ? attribute[1] : attribute;
|
||||
const selectors = getSelector(prefix, attributeName, null);
|
||||
addVariants(variantName, selectors, addVariant, i);
|
||||
}
|
||||
});
|
8
src/vendor/react-aria-components.ts
vendored
8
src/vendor/react-aria-components.ts
vendored
@ -1,5 +1,5 @@
|
||||
// https://github.com/adobe/react-spectrum/blob/14f324fe890fcedc6e34889d9b04d5d6bfeb8380/packages/tailwindcss-react-aria-components/src/index.js
|
||||
import plugin from "tailwindcss/plugin";
|
||||
import plugin from "tailwindcss/plugin.js";
|
||||
import type { PluginAPI } from "tailwindcss/types/config";
|
||||
|
||||
// Order of these is important because it determines which states win in a conflict.
|
||||
@ -43,7 +43,7 @@ const attributes = {
|
||||
"drop-target",
|
||||
"resizing",
|
||||
"disabled",
|
||||
],
|
||||
] as const,
|
||||
enum: {
|
||||
placement: ["left", "right", "top", "bottom"],
|
||||
type: ["literal", "year", "month", "day"],
|
||||
@ -167,7 +167,9 @@ export default plugin.withOptions<{ prefix: string }>(
|
||||
!!future?.hoverOnlyWhenSupported);
|
||||
|
||||
// Enum attributes go first because currently they are all non-interactive states.
|
||||
for (const [attributeName, value] of Object.entries(attributes.enum)) {
|
||||
for (const [attributeName, value] of Object.entries(
|
||||
attributes.enum
|
||||
) as (keyof typeof attributes.enum)[][]) {
|
||||
for (const attributeValue of value) {
|
||||
const name = shortNames[attributeName] || attributeName;
|
||||
const variantName = `${prefix}${name}-${attributeValue}`;
|
||||
|
8
src/vendor/typography.ts
vendored
8
src/vendor/typography.ts
vendored
@ -1,8 +1,8 @@
|
||||
// https://github.com/tailwindlabs/tailwindcss-typography/commit/7b43b3b33bb74c57a68852330105bb34d11a806a
|
||||
import plugin from "tailwindcss/plugin";
|
||||
import colors from "tailwindcss/colors";
|
||||
import { castArray, merge } from "lodash";
|
||||
// https://github.com/tailwindlabs/tailwindcss-typography/commit/d1e6421d4c07c15b3e1db6b6b10549df96fb129d
|
||||
import { castArray, merge } from "lodash-es";
|
||||
import parser, { type Pseudo } from "postcss-selector-parser";
|
||||
import colors from "tailwindcss/colors.js";
|
||||
import plugin from "tailwindcss/plugin.js";
|
||||
|
||||
const parseSelector = parser();
|
||||
|
||||
|
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 type * as vite from "vite";
|
||||
|
||||
import { type Compile, type StyleMap, macroNames, pkgName } from "./shared";
|
||||
|
||||
import type { BuildStyleFile } from "./index";
|
||||
|
||||
const ROLLUP_PREFIX = "\0tailwind:";
|
||||
@ -17,7 +20,7 @@ export const vitePlugin = ({
|
||||
name: "tailwind",
|
||||
|
||||
config(config) {
|
||||
((config.optimizeDeps ?? {}).exclude ?? []).push(...macroNames, `${pkgName}/base`);
|
||||
((config.optimizeDeps ??= {}).exclude ??= []).push(...macroNames, `${pkgName}/base`);
|
||||
},
|
||||
|
||||
resolveId(id, importer) {
|
||||
@ -59,4 +62,4 @@ export const vitePlugin = ({
|
||||
* `babel-plugin-macros` compatible `isMacrosName` function that works with this plugin.
|
||||
*/
|
||||
export const isMacrosName = (v: string) =>
|
||||
!macroNames.includes(v) && /[./]macro(\.c?js)?$/.test(v);
|
||||
!macroNames.includes(v) && /[./]macro(?:\.c?js)?$/.test(v);
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
import pkg from "./package.json" with { type: "json" };
|
||||
|
||||
export default defineConfig({
|
||||
test: {},
|
||||
define: {
|
||||
__PKG_NAME__: JSON.stringify(pkg.name),
|
||||
},
|
||||
});
|
||||
|
Reference in New Issue
Block a user