Compare commits
3 Commits
f741ba41c2
...
2c4b75aa6c
Author | SHA1 | Date | |
---|---|---|---|
|
2c4b75aa6c | ||
|
4db894d061 | ||
|
5a3b03b69b |
@ -1,8 +0,0 @@
|
|||||||
// @ts-check
|
|
||||||
const { extendConfig } = require("@aet/eslint-rules");
|
|
||||||
|
|
||||||
module.exports = extendConfig({
|
|
||||||
rules: {
|
|
||||||
"class-methods-use-this": "error",
|
|
||||||
},
|
|
||||||
});
|
|
@ -6,4 +6,6 @@ Compile-run Tailwind compiler.
|
|||||||
export function App() {
|
export function App() {
|
||||||
return <div css="flex m-0"></div>;
|
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"] }],
|
||||||
|
},
|
||||||
|
});
|
57
package.json
57
package.json
@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "@aet/tailwind",
|
"name": "@aet/tailwind",
|
||||||
"version": "1.0.9",
|
"version": "1.0.20",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "./scripts/index.ts",
|
"build": "./scripts/index.ts",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
@ -13,7 +14,11 @@
|
|||||||
"exports": {
|
"exports": {
|
||||||
".": "./dist/index.js",
|
".": "./dist/index.js",
|
||||||
"./package.json": "./package.json",
|
"./package.json": "./package.json",
|
||||||
"./classed": "./dist/classed.mjs",
|
"./classed": "./dist/classed.js",
|
||||||
|
"./css-to-js": "./dist/css-to-js.js",
|
||||||
|
"./base": {
|
||||||
|
"types": "./dist/base.d.ts"
|
||||||
|
},
|
||||||
"./µ": {
|
"./µ": {
|
||||||
"types": "./dist/macro.d.ts"
|
"types": "./dist/macro.d.ts"
|
||||||
},
|
},
|
||||||
@ -28,46 +33,46 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@aet/eslint-rules": "1.0.1-beta.24",
|
"@aet/eslint-rules": "2.0.35",
|
||||||
"@types/babel__core": "^7.20.5",
|
"@types/babel__core": "^7.20.5",
|
||||||
"@types/bun": "^1.1.6",
|
"@types/bun": "^1.2.0",
|
||||||
"@types/dedent": "^0.7.2",
|
"@types/dedent": "^0.7.2",
|
||||||
"@types/lodash": "^4.17.7",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^22.2.0",
|
"@types/node": "^22.13.0",
|
||||||
"@types/postcss-safe-parser": "^5.0.4",
|
"@types/postcss-safe-parser": "^5.0.4",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^19.0.8",
|
||||||
"@types/stylis": "^4.2.6",
|
"@types/stylis": "^4.2.7",
|
||||||
"cli-highlight": "^2.1.11",
|
"cli-highlight": "^2.1.11",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"colord": "^2.9.3",
|
"colord": "^2.9.3",
|
||||||
"css-what": "^6.1.0",
|
"css-what": "^6.1.0",
|
||||||
"dedent": "^1.5.3",
|
"dedent": "^1.5.3",
|
||||||
"esbuild-register": "^3.6.0",
|
"esbuild-register": "^3.6.0",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^9.19.0",
|
||||||
"nolyfill": "^1.0.39",
|
"nolyfill": "^1.0.43",
|
||||||
"postcss-nested": "^6.2.0",
|
"postcss-nested": "^7.0.2",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.4.2",
|
||||||
"tailwindcss": "^3.4.9",
|
"tailwindcss": "^3.4.17",
|
||||||
"tslib": "^2.6.3",
|
"tslib": "^2.8.1",
|
||||||
"tsup": "^8.2.4",
|
"tsup": "^8.3.6",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.7.3",
|
||||||
"vite": "^5.4.0",
|
"vite": "^6.0.11",
|
||||||
"vitest": "^2.0.5"
|
"vitest": "^3.0.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"tailwindcss": "^3.4.3"
|
"tailwindcss": "^3.4.17"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.26.7",
|
||||||
"@emotion/hash": "^0.9.2",
|
"@emotion/hash": "^0.9.2",
|
||||||
"esbuild": "^0.23.0",
|
"esbuild": "^0.24.2",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"postcss": "^8.4.41",
|
"postcss": "^8.5.1",
|
||||||
"postcss-selector-parser": "^6.1.1",
|
"postcss-selector-parser": "^7.0.0",
|
||||||
"stylis": "^4.3.2",
|
"stylis": "^4.3.5",
|
||||||
"tiny-invariant": "^1.3.3",
|
"tiny-invariant": "^1.3.3",
|
||||||
"type-fest": "^4.24.0"
|
"type-fest": "^4.33.0"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"arrowParens": "avoid",
|
"arrowParens": "avoid",
|
||||||
|
3556
pnpm-lock.yaml
generated
3556
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,9 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
import { promises as fs } from "node:fs";
|
import { promises as fs } from "node:fs";
|
||||||
|
|
||||||
|
import { pick } from "lodash-es";
|
||||||
import { build, defineConfig } from "tsup";
|
import { build, defineConfig } from "tsup";
|
||||||
import { pick } from "lodash";
|
|
||||||
import pkg from "../package.json" with { type: "json" };
|
import pkg from "../package.json" with { type: "json" };
|
||||||
|
|
||||||
const tsupConfig = defineConfig({
|
const tsupConfig = defineConfig({
|
||||||
@ -11,19 +13,29 @@ const tsupConfig = defineConfig({
|
|||||||
dts: true,
|
dts: true,
|
||||||
treeshake: true,
|
treeshake: true,
|
||||||
platform: "node",
|
platform: "node",
|
||||||
|
format: "esm",
|
||||||
banner: {
|
banner: {
|
||||||
js: "/* eslint-disable */",
|
js: "/* eslint-disable */",
|
||||||
},
|
},
|
||||||
|
define: {
|
||||||
|
__PKG_NAME__: JSON.stringify(pkg.name),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await build({
|
||||||
|
...tsupConfig,
|
||||||
|
entry: ["src/classed.tsx", "src/css-to-js.ts"],
|
||||||
|
outDir: "dist",
|
||||||
|
external: ["react", "react/jsx-runtime", "clsx"],
|
||||||
|
clean: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
build({
|
build({
|
||||||
...tsupConfig,
|
...tsupConfig,
|
||||||
entry: ["src/classed.tsx"],
|
entry: ["src/classed.tsx", "src/css-to-js.ts"],
|
||||||
outDir: "dist",
|
outDir: "dist",
|
||||||
external: ["react", "react/jsx-runtime", "clsx"],
|
external: ["react", "react/jsx-runtime", "clsx"],
|
||||||
format: "esm",
|
|
||||||
clean: true,
|
|
||||||
}),
|
}),
|
||||||
build({
|
build({
|
||||||
...tsupConfig,
|
...tsupConfig,
|
||||||
@ -48,10 +60,18 @@ await Promise.all([
|
|||||||
Bun.write(
|
Bun.write(
|
||||||
"dist/package.json",
|
"dist/package.json",
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
pick(pkg, ["name", "version", "license", "dependencies", "author"]),
|
pick(pkg, [
|
||||||
|
"name",
|
||||||
|
"version",
|
||||||
|
"type",
|
||||||
|
"license",
|
||||||
|
"dependencies",
|
||||||
|
"author",
|
||||||
|
"exports",
|
||||||
|
]),
|
||||||
null,
|
null,
|
||||||
2
|
2
|
||||||
)
|
).replaceAll("./dist/", "./")
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
67
scripts/update-tags.ts
Executable file
67
scripts/update-tags.ts
Executable file
@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
import fs from "node:fs";
|
||||||
|
|
||||||
|
const supportedTags = [
|
||||||
|
"a",
|
||||||
|
"abbr",
|
||||||
|
"article",
|
||||||
|
"blockquote",
|
||||||
|
"button",
|
||||||
|
"cite",
|
||||||
|
"code",
|
||||||
|
"details",
|
||||||
|
"div",
|
||||||
|
"figure",
|
||||||
|
"footer",
|
||||||
|
"h1",
|
||||||
|
"h2",
|
||||||
|
"h3",
|
||||||
|
"h4",
|
||||||
|
"hr",
|
||||||
|
"iframe",
|
||||||
|
"img",
|
||||||
|
"input",
|
||||||
|
"ins",
|
||||||
|
"label",
|
||||||
|
"li",
|
||||||
|
"main",
|
||||||
|
"nav",
|
||||||
|
"ol",
|
||||||
|
"output",
|
||||||
|
"p",
|
||||||
|
"pre",
|
||||||
|
"rt",
|
||||||
|
"ruby",
|
||||||
|
"section",
|
||||||
|
"select",
|
||||||
|
"span",
|
||||||
|
"strong",
|
||||||
|
"sub",
|
||||||
|
"summary",
|
||||||
|
"sup",
|
||||||
|
"table",
|
||||||
|
"tbody",
|
||||||
|
"td",
|
||||||
|
"thead",
|
||||||
|
"tr",
|
||||||
|
"ul",
|
||||||
|
"var",
|
||||||
|
"video",
|
||||||
|
].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]);`
|
||||||
|
);
|
@ -39,3 +39,14 @@ exports[`babel-tailwind > supports grouped tw 2`] = `
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`babel-tailwind > supports styled components usage 1`] = `
|
||||||
|
"import { classed as _classed } from "@aet/tailwind/classed";
|
||||||
|
var Div = _classed("div", "tw-gqn2k6");"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`babel-tailwind > supports styled components usage 2`] = `
|
||||||
|
".tw-gqn2k6 {
|
||||||
|
text-align: center;
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
@ -40,4 +40,20 @@ describe("babel-tailwind", () => {
|
|||||||
|
|
||||||
matchSnapshot(files);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -47,7 +47,14 @@ export function getBuild(name: string) {
|
|||||||
const result = await esbuild.build({
|
const result = await esbuild.build({
|
||||||
bundle: true,
|
bundle: true,
|
||||||
write: false,
|
write: false,
|
||||||
external: ["react/jsx-runtime", "@emotion/css", "clsx", "tslib"],
|
external: [
|
||||||
|
"react",
|
||||||
|
"react/jsx-runtime",
|
||||||
|
"@emotion/css",
|
||||||
|
"clsx",
|
||||||
|
"tslib",
|
||||||
|
"@aet/tailwind/classed",
|
||||||
|
],
|
||||||
outdir: "dist",
|
outdir: "dist",
|
||||||
format: "esm",
|
format: "esm",
|
||||||
entryPoints: [await write("index.tsx", dedent(javascript))],
|
entryPoints: [await write("index.tsx", dedent(javascript))],
|
||||||
|
@ -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));
|
|
405
src/babel/index.ts
Normal file
405
src/babel/index.ts
Normal file
@ -0,0 +1,405 @@
|
|||||||
|
import { basename, dirname, extname, join } from "node:path";
|
||||||
|
|
||||||
|
import type b from "@babel/core";
|
||||||
|
import { type NodePath, type types as t } from "@babel/core";
|
||||||
|
import hash from "@emotion/hash";
|
||||||
|
import invariant from "tiny-invariant";
|
||||||
|
|
||||||
|
import { type ResolveTailwindOptions, getClassName } from "../index";
|
||||||
|
import { type SourceLocation, type StyleMapEntry, classedName } 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";
|
||||||
|
|
||||||
|
export type BabelPluginUtils = ReturnType<typeof getUtils>;
|
||||||
|
|
||||||
|
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,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
let cx: t.Identifier;
|
||||||
|
let tslibImport: t.Identifier;
|
||||||
|
let styleImport: t.Identifier;
|
||||||
|
let classedImport: 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);
|
||||||
|
},
|
||||||
|
|
||||||
|
getClassedImport: () => {
|
||||||
|
if (classedImport == null) {
|
||||||
|
classedImport = path.scope.generateUidIdentifier("classed");
|
||||||
|
path.node.body.unshift(
|
||||||
|
t.importDeclaration(
|
||||||
|
[t.importSpecifier(classedImport, t.identifier("classed"))],
|
||||||
|
t.stringLiteral(classedName)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return t.cloneNode(classedImport);
|
||||||
|
},
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function babelTailwind(
|
||||||
|
options: ResolveTailwindOptions,
|
||||||
|
onCollect: ClassNameCollector | undefined
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
getClassName: getClass = getClassName,
|
||||||
|
jsxAttributeAction = "delete",
|
||||||
|
jsxAttributeName = "css",
|
||||||
|
} = 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(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") {
|
||||||
|
// 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.importDefaultSpecifier(cx)], 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}`;
|
||||||
|
}
|
210
src/babel/macro.ts
Normal file
210
src/babel/macro.ts
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
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, 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("css", list.join(" "));
|
||||||
|
_.recordIfAbsent(type, {
|
||||||
|
key: className,
|
||||||
|
classNames: list,
|
||||||
|
location: _.sliceText(node),
|
||||||
|
});
|
||||||
|
_.replaceWithImport({
|
||||||
|
type,
|
||||||
|
path: callee,
|
||||||
|
className: addIf(className, list.includes("group") && " group"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
"article",
|
||||||
|
"blockquote",
|
||||||
|
"button",
|
||||||
|
"cite",
|
||||||
|
"code",
|
||||||
|
"details",
|
||||||
|
"div",
|
||||||
|
"figure",
|
||||||
|
"footer",
|
||||||
|
"h1",
|
||||||
|
"h2",
|
||||||
|
"h3",
|
||||||
|
"h4",
|
||||||
|
"hr",
|
||||||
|
"iframe",
|
||||||
|
"img",
|
||||||
|
"input",
|
||||||
|
"ins",
|
||||||
|
"label",
|
||||||
|
"li",
|
||||||
|
"main",
|
||||||
|
"nav",
|
||||||
|
"ol",
|
||||||
|
"output",
|
||||||
|
"p",
|
||||||
|
"pre",
|
||||||
|
"rt",
|
||||||
|
"ruby",
|
||||||
|
"section",
|
||||||
|
"select",
|
||||||
|
"span",
|
||||||
|
"strong",
|
||||||
|
"sub",
|
||||||
|
"summary",
|
||||||
|
"sup",
|
||||||
|
"table",
|
||||||
|
"tbody",
|
||||||
|
"td",
|
||||||
|
"thead",
|
||||||
|
"tr",
|
||||||
|
"ul",
|
||||||
|
"var",
|
||||||
|
"video",
|
||||||
|
]);
|
45
src/babel/utils.ts
Normal file
45
src/babel/utils.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { isPlainObject } from "lodash-es";
|
||||||
|
import invariant from "tiny-invariant";
|
||||||
|
import { type NodePath } from "@babel/core";
|
||||||
|
|
||||||
|
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,5 @@
|
|||||||
import { type FunctionComponent, forwardRef } from "react";
|
|
||||||
import cx from "clsx";
|
import cx from "clsx";
|
||||||
|
import { type FunctionComponent, forwardRef } from "react";
|
||||||
|
|
||||||
export interface WithClassName<Props = object> extends FunctionComponent<Props> {
|
export interface WithClassName<Props = object> extends FunctionComponent<Props> {
|
||||||
className: string;
|
className: string;
|
||||||
@ -23,11 +23,11 @@ export const classed: {
|
|||||||
className: PresetClassName<InputProps>,
|
className: PresetClassName<InputProps>,
|
||||||
defaultProps?: Partial<InputProps>
|
defaultProps?: Partial<InputProps>
|
||||||
): React.FunctionComponent<InputProps>;
|
): React.FunctionComponent<InputProps>;
|
||||||
<K extends keyof JSX.IntrinsicElements>(
|
<K extends keyof React.JSX.IntrinsicElements>(
|
||||||
type: K,
|
type: K,
|
||||||
className: PresetClassName<JSX.IntrinsicElements[K]>,
|
className: PresetClassName<React.JSX.IntrinsicElements[K]>,
|
||||||
defaultProps?: Partial<JSX.IntrinsicElements[K]>
|
defaultProps?: Partial<React.JSX.IntrinsicElements[K]>
|
||||||
): React.FunctionComponent<JSX.IntrinsicElements[K]>;
|
): React.FunctionComponent<React.JSX.IntrinsicElements[K]>;
|
||||||
(
|
(
|
||||||
type: string,
|
type: string,
|
||||||
className: PresetClassName,
|
className: PresetClassName,
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
// MIT License. Copyright (c) 2017 Brice BERNARD
|
// MIT License. Copyright (c) 2017 Brice BERNARD
|
||||||
// https://github.com/brikou/CSS-in-JS-generator/commit/2a887d0d96f1d5044039d0e0457001f0fde0def0
|
// https://github.com/brikou/CSS-in-JS-generator/commit/2a887d0d96f1d5044039d0e0457001f0fde0def0
|
||||||
|
import JSON5 from "json5";
|
||||||
|
import { camelCase } from "lodash-es";
|
||||||
import {
|
import {
|
||||||
type AtRule,
|
type AtRule,
|
||||||
type Builder,
|
type Builder,
|
||||||
@ -8,11 +10,9 @@ import {
|
|||||||
type Rule,
|
type Rule,
|
||||||
parse,
|
parse,
|
||||||
} from "postcss";
|
} from "postcss";
|
||||||
import JSON5 from "json5";
|
|
||||||
import parseSelector from "postcss-selector-parser";
|
|
||||||
import Stringifier from "postcss/lib/stringifier";
|
import Stringifier from "postcss/lib/stringifier";
|
||||||
|
import parseSelector from "postcss-selector-parser";
|
||||||
import { type Element, compile } from "stylis";
|
import { type Element, compile } from "stylis";
|
||||||
import { camelCase } from "lodash";
|
|
||||||
|
|
||||||
function getSelectorScope(selector: string): string {
|
function getSelectorScope(selector: string): string {
|
||||||
let selectorScope = "root";
|
let selectorScope = "root";
|
||||||
@ -129,7 +129,8 @@ const convertScopeToModuleName = (scope: string) =>
|
|||||||
"_$1"
|
"_$1"
|
||||||
);
|
);
|
||||||
|
|
||||||
export function convertCssToJS(
|
/** @internal */
|
||||||
|
export function toJSCode(
|
||||||
css: string,
|
css: string,
|
||||||
mapClassNames: (className: string) => string = convertScopeToModuleName
|
mapClassNames: (className: string) => string = convertScopeToModuleName
|
||||||
): string {
|
): string {
|
||||||
@ -211,7 +212,7 @@ export function convertCssToJS(
|
|||||||
? `injectGlobal\`${style}\n\`;\n`
|
? `injectGlobal\`${style}\n\`;\n`
|
||||||
: `\nexport const ${mapClassNames(
|
: `\nexport const ${mapClassNames(
|
||||||
scope
|
scope
|
||||||
)} = ${JSON5.stringify(asJSObject(style), null, 2)};\n`;
|
)} = ${JSON5.stringify(cssToJS(style), null, 2)};\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.trim();
|
return res.trim();
|
||||||
@ -299,7 +300,7 @@ function simplifyValue(propName: string, value: string) {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function asJSObject(inputCssText: string) {
|
export function cssToJS(inputCssText: string): Record<string, any> {
|
||||||
const css = compile(inputCssText);
|
const css = compile(inputCssText);
|
||||||
const result: Record<string, string> = {};
|
const result: Record<string, string> = {};
|
||||||
|
|
||||||
@ -338,5 +339,5 @@ function asJSObject(inputCssText: string) {
|
|||||||
for (const node of css) {
|
for (const node of css) {
|
||||||
walk(result, node);
|
walk(result, node);
|
||||||
}
|
}
|
||||||
return result as React.CSSProperties;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { extname } from "node:path";
|
import { extname } from "node:path";
|
||||||
import { once } from "lodash";
|
import { once } from "lodash-es";
|
||||||
import type babel from "@babel/core";
|
import type babel from "@babel/core";
|
||||||
import type * as esbuild from "esbuild";
|
import type * as esbuild from "esbuild";
|
||||||
import { transformSync } from "@babel/core";
|
import { transformSync } from "@babel/core";
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
|
|
||||||
import type * as esbuild from "esbuild";
|
import type * as esbuild from "esbuild";
|
||||||
import { CssSyntaxError } from "postcss";
|
import { CssSyntaxError } from "postcss";
|
||||||
|
|
||||||
import { type Compile, type StyleMap, type StyleMapEntry, pkgName } from "./shared";
|
import { type Compile, type StyleMap, type StyleMapEntry, pkgName } from "./shared";
|
||||||
|
|
||||||
import type { BuildStyleFile } from "./index";
|
import type { BuildStyleFile } from "./index";
|
||||||
|
|
||||||
const PLUGIN_NAME = "tailwind";
|
const PLUGIN_NAME = "tailwind";
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { getClassName } from "./index";
|
|
||||||
import { getBuild } from "./__tests__/utils";
|
import { getBuild } from "./__tests__/utils";
|
||||||
|
|
||||||
|
import { getClassName } from "./index";
|
||||||
|
|
||||||
describe("babel-tailwind", () => {
|
describe("babel-tailwind", () => {
|
||||||
const compileESBuild = getBuild("main");
|
const compileESBuild = getBuild("main");
|
||||||
|
|
||||||
|
24
src/index.ts
24
src/index.ts
@ -1,14 +1,15 @@
|
|||||||
import hash from "@emotion/hash";
|
import hash from "@emotion/hash";
|
||||||
|
import { transformSync } from "esbuild";
|
||||||
|
import { memoize, without } from "lodash-es";
|
||||||
|
import type postcss from "postcss";
|
||||||
import type { Config } from "tailwindcss";
|
import type { Config } from "tailwindcss";
|
||||||
import type { SetRequired } from "type-fest";
|
import type { SetRequired } from "type-fest";
|
||||||
import { transformSync } from "esbuild";
|
|
||||||
import type postcss from "postcss";
|
import { type ClassNameCollector, babelTailwind } from "./babel/index";
|
||||||
import { memoize, without } from "lodash";
|
import { toJSCode } from "./css-to-js";
|
||||||
import { type ClassNameCollector, babelTailwind } from "./babel-tailwind";
|
|
||||||
import { esbuildPlugin } from "./esbuild-postcss";
|
import { esbuildPlugin } from "./esbuild-postcss";
|
||||||
import { vitePlugin } from "./vite-plugin";
|
|
||||||
import { type StyleMap, createPostCSS } from "./shared";
|
import { type StyleMap, createPostCSS } from "./shared";
|
||||||
import { convertCssToJS } from "./css-to-js";
|
import { vitePlugin } from "./vite-plugin";
|
||||||
|
|
||||||
export { isMacrosName } from "./vite-plugin";
|
export { isMacrosName } from "./vite-plugin";
|
||||||
|
|
||||||
@ -120,7 +121,7 @@ export const getClassName: GetClassName = cls => "tw-" + hash(cls);
|
|||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
export function getTailwindPlugins(options: TailwindPluginOptions) {
|
export function getTailwindPlugins(options: TailwindPluginOptions) {
|
||||||
const { addSourceAsComment } = options;
|
const { addSourceAsComment, compile: _compile } = options;
|
||||||
const resolvedOptions: ResolveTailwindOptions = {
|
const resolvedOptions: ResolveTailwindOptions = {
|
||||||
getClassName,
|
getClassName,
|
||||||
jsxAttributeAction: "delete",
|
jsxAttributeAction: "delete",
|
||||||
@ -134,7 +135,7 @@ export function getTailwindPlugins(options: TailwindPluginOptions) {
|
|||||||
const getCompiler = () => createPostCSS(resolvedOptions);
|
const getCompiler = () => createPostCSS(resolvedOptions);
|
||||||
|
|
||||||
const { styleMap } = resolvedOptions;
|
const { styleMap } = resolvedOptions;
|
||||||
const compile = options.compile ?? memoize(getCompiler());
|
const compile = _compile ?? memoize(getCompiler());
|
||||||
|
|
||||||
const buildStyleFile: BuildStyleFile = async path => {
|
const buildStyleFile: BuildStyleFile = async path => {
|
||||||
const styles = styleMap.get(path)!;
|
const styles = styleMap.get(path)!;
|
||||||
@ -156,7 +157,7 @@ export function getTailwindPlugins(options: TailwindPluginOptions) {
|
|||||||
if (path.endsWith(".css")) {
|
if (path.endsWith(".css")) {
|
||||||
return ["css", transformSync(compiled, { loader: "css" }).code] as const;
|
return ["css", transformSync(compiled, { loader: "css" }).code] as const;
|
||||||
} else if (path.endsWith(".js")) {
|
} 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;
|
return ["js", js] as const;
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Unknown file extension");
|
throw new Error("Unknown file extension");
|
||||||
@ -168,7 +169,10 @@ export function getTailwindPlugins(options: TailwindPluginOptions) {
|
|||||||
babel: (onCollect?: ClassNameCollector) => babelTailwind(resolvedOptions, onCollect),
|
babel: (onCollect?: ClassNameCollector) => babelTailwind(resolvedOptions, onCollect),
|
||||||
esbuild: () => esbuildPlugin({ styleMap, compile, buildStyleFile }),
|
esbuild: () => esbuildPlugin({ styleMap, compile, buildStyleFile }),
|
||||||
/** Requires `options.vite` to be `true`. */
|
/** Requires `options.vite` to be `true`. */
|
||||||
vite: () => vitePlugin({ styleMap, compile, buildStyleFile }),
|
vite: () => {
|
||||||
|
resolvedOptions.vite = true;
|
||||||
|
return vitePlugin({ styleMap, compile, buildStyleFile });
|
||||||
|
},
|
||||||
styleMap,
|
styleMap,
|
||||||
options,
|
options,
|
||||||
getCompiler,
|
getCompiler,
|
||||||
|
53
src/macro.d.ts
vendored
53
src/macro.d.ts
vendored
@ -10,6 +10,53 @@ interface RecursiveStringObject {
|
|||||||
|
|
||||||
type CSSAttributeValue = string | (string | RecursiveStringObject)[];
|
type CSSAttributeValue = string | (string | RecursiveStringObject)[];
|
||||||
|
|
||||||
|
export type SupportedTag =
|
||||||
|
| "a"
|
||||||
|
| "abbr"
|
||||||
|
| "article"
|
||||||
|
| "blockquote"
|
||||||
|
| "button"
|
||||||
|
| "cite"
|
||||||
|
| "code"
|
||||||
|
| "details"
|
||||||
|
| "div"
|
||||||
|
| "figure"
|
||||||
|
| "footer"
|
||||||
|
| "h1"
|
||||||
|
| "h2"
|
||||||
|
| "h3"
|
||||||
|
| "h4"
|
||||||
|
| "hr"
|
||||||
|
| "iframe"
|
||||||
|
| "img"
|
||||||
|
| "input"
|
||||||
|
| "ins"
|
||||||
|
| "label"
|
||||||
|
| "li"
|
||||||
|
| "main"
|
||||||
|
| "nav"
|
||||||
|
| "ol"
|
||||||
|
| "output"
|
||||||
|
| "p"
|
||||||
|
| "pre"
|
||||||
|
| "rt"
|
||||||
|
| "ruby"
|
||||||
|
| "section"
|
||||||
|
| "select"
|
||||||
|
| "span"
|
||||||
|
| "strong"
|
||||||
|
| "sub"
|
||||||
|
| "summary"
|
||||||
|
| "sup"
|
||||||
|
| "table"
|
||||||
|
| "tbody"
|
||||||
|
| "td"
|
||||||
|
| "thead"
|
||||||
|
| "tr"
|
||||||
|
| "ul"
|
||||||
|
| "var"
|
||||||
|
| "video";
|
||||||
|
|
||||||
type Modifier =
|
type Modifier =
|
||||||
| "2xl"
|
| "2xl"
|
||||||
| "active"
|
| "active"
|
||||||
@ -69,6 +116,10 @@ export type TailwindFunction = {
|
|||||||
(...args: (string | RecursiveStringObject)[]): string;
|
(...args: (string | RecursiveStringObject)[]): string;
|
||||||
} & {
|
} & {
|
||||||
[key in Modifier]: TailwindFunction;
|
[key in Modifier]: TailwindFunction;
|
||||||
|
} & {
|
||||||
|
[tag in SupportedTag]: (
|
||||||
|
strings: TemplateStringsArray
|
||||||
|
) => React.FunctionComponent<React.JSX.IntrinsicElements[tag]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -99,5 +150,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 {}
|
export interface Config {}
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import tailwind from "tailwindcss";
|
|
||||||
import postcss from "postcss";
|
import postcss from "postcss";
|
||||||
|
import tailwind from "tailwindcss";
|
||||||
|
|
||||||
import type { ResolveTailwindOptions } from "./index";
|
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 macroNames = [`${pkgName}/macro`, `${pkgName}/µ`];
|
||||||
|
export const classedName = `${pkgName}/classed`;
|
||||||
|
|
||||||
interface LineColumn {
|
interface LineColumn {
|
||||||
line: number;
|
line: number;
|
||||||
|
2
src/vendor/animate.ts
vendored
2
src/vendor/animate.ts
vendored
@ -1,5 +1,5 @@
|
|||||||
// https://github.com/jamiebuilds/tailwindcss-animate/commit/ac0dd3a3c81681b78f1d8ea5e7478044213995e1
|
// https://github.com/jamiebuilds/tailwindcss-animate/commit/ac0dd3a3c81681b78f1d8ea5e7478044213995e1
|
||||||
import plugin from "tailwindcss/plugin";
|
import plugin from "tailwindcss/plugin.js";
|
||||||
import type { PluginAPI } from "tailwindcss/types/config";
|
import type { PluginAPI } from "tailwindcss/types/config";
|
||||||
|
|
||||||
function filterDefault<T extends object>(values: T) {
|
function filterDefault<T extends object>(values: T) {
|
||||||
|
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
|
// https://github.com/tailwindlabs/tailwindcss-aspect-ratio/commit/b2a9d02229946f3430c0013198be2affa7a175da
|
||||||
import plugin from "tailwindcss/plugin";
|
import plugin from "tailwindcss/plugin.js";
|
||||||
|
|
||||||
const baseStyles = {
|
const baseStyles = {
|
||||||
position: "relative",
|
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
|
// https://github.com/tailwindlabs/tailwindcss-container-queries/commit/ef92eba7a7df60659da1ab8dd584346f00efae73
|
||||||
import plugin from "tailwindcss/plugin";
|
import plugin from "tailwindcss/plugin.js";
|
||||||
|
|
||||||
function parseValue(value: string) {
|
function parseValue(value: string) {
|
||||||
const numericValue = value.match(/^(\d+\.\d+|\d+|\.\d+)\D+/)?.[1] ?? null;
|
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
|
// https://github.com/tailwindlabs/tailwindcss-forms/commit/c9d9da3e010b194a1f0e9c36fbd98c83e4762840
|
||||||
import plugin from "tailwindcss/plugin";
|
import colors from "tailwindcss/colors.js";
|
||||||
import defaultTheme from "tailwindcss/defaultTheme";
|
import defaultTheme from "tailwindcss/defaultTheme.js";
|
||||||
import colors from "tailwindcss/colors";
|
import plugin from "tailwindcss/plugin.js";
|
||||||
import type { CSSRuleObject } from "tailwindcss/types/config";
|
import type { CSSRuleObject } from "tailwindcss/types/config";
|
||||||
|
|
||||||
const shorterNames = {
|
const shorterNames = {
|
||||||
@ -125,6 +125,16 @@ type Strategy = "base" | "class";
|
|||||||
export default plugin.withOptions<{ strategy?: Strategy }>(
|
export default plugin.withOptions<{ strategy?: Strategy }>(
|
||||||
options =>
|
options =>
|
||||||
function ({ addBase, addComponents, theme }) {
|
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 =
|
const strategy =
|
||||||
options?.strategy === undefined ? ["base", "class"] : [options.strategy];
|
options?.strategy === undefined ? ["base", "class"] : [options.strategy];
|
||||||
|
|
||||||
@ -259,7 +269,7 @@ export default plugin.withOptions<{ strategy?: Strategy }>(
|
|||||||
class: [".form-select"],
|
class: [".form-select"],
|
||||||
styles: {
|
styles: {
|
||||||
"background-image": `url("${svgToDataUri(
|
"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",
|
||||||
colors.gray[500]
|
colors.gray[500]
|
||||||
)}" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6 8l4 4 4-4"/></svg>`
|
)}" 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
|
// 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";
|
import type { PluginAPI } from "tailwindcss/types/config";
|
||||||
|
|
||||||
// Order of these is important because it determines which states win in a conflict.
|
// Order of these is important because it determines which states win in a conflict.
|
||||||
@ -43,7 +43,7 @@ const attributes = {
|
|||||||
"drop-target",
|
"drop-target",
|
||||||
"resizing",
|
"resizing",
|
||||||
"disabled",
|
"disabled",
|
||||||
],
|
] as const,
|
||||||
enum: {
|
enum: {
|
||||||
placement: ["left", "right", "top", "bottom"],
|
placement: ["left", "right", "top", "bottom"],
|
||||||
type: ["literal", "year", "month", "day"],
|
type: ["literal", "year", "month", "day"],
|
||||||
@ -167,7 +167,9 @@ export default plugin.withOptions<{ prefix: string }>(
|
|||||||
!!future?.hoverOnlyWhenSupported);
|
!!future?.hoverOnlyWhenSupported);
|
||||||
|
|
||||||
// Enum attributes go first because currently they are all non-interactive states.
|
// 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) {
|
for (const attributeValue of value) {
|
||||||
const name = shortNames[attributeName] || attributeName;
|
const name = shortNames[attributeName] || attributeName;
|
||||||
const variantName = `${prefix}${name}-${attributeValue}`;
|
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
|
// https://github.com/tailwindlabs/tailwindcss-typography/commit/d1e6421d4c07c15b3e1db6b6b10549df96fb129d
|
||||||
import plugin from "tailwindcss/plugin";
|
import { castArray, merge } from "lodash-es";
|
||||||
import colors from "tailwindcss/colors";
|
|
||||||
import { castArray, merge } from "lodash";
|
|
||||||
import parser, { type Pseudo } from "postcss-selector-parser";
|
import parser, { type Pseudo } from "postcss-selector-parser";
|
||||||
|
import colors from "tailwindcss/colors.js";
|
||||||
|
import plugin from "tailwindcss/plugin.js";
|
||||||
|
|
||||||
const parseSelector = parser();
|
const parseSelector = parser();
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ export const vitePlugin = ({
|
|||||||
name: "tailwind",
|
name: "tailwind",
|
||||||
|
|
||||||
config(config) {
|
config(config) {
|
||||||
((config.optimizeDeps ?? {}).exclude ?? []).push(...macroNames, `${pkgName}/base`);
|
((config.optimizeDeps ??= {}).exclude ??= []).push(...macroNames, `${pkgName}/base`);
|
||||||
},
|
},
|
||||||
|
|
||||||
resolveId(id, importer) {
|
resolveId(id, importer) {
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
import pkg from "./package.json" with { type: "json" };
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {},
|
test: {},
|
||||||
|
define: {
|
||||||
|
__PKG_NAME__: JSON.stringify(pkg.name),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user