Optimization
This commit is contained in:
parent
1f5e7fa049
commit
08cd7940e2
15
package.json
15
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@aet/tailwind",
|
||||
"version": "1.0.23",
|
||||
"version": "1.0.24",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@ -33,12 +33,13 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aet/eslint-rules": "2.0.35",
|
||||
"@aet/eslint-rules": "2.0.36",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"@types/bun": "^1.2.0",
|
||||
"@types/babel__traverse": "^7.20.6",
|
||||
"@types/bun": "^1.2.2",
|
||||
"@types/dedent": "^0.7.2",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.13.0",
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/postcss-safe-parser": "^5.0.4",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/stylis": "^4.2.7",
|
||||
@ -56,8 +57,8 @@
|
||||
"tslib": "^2.8.1",
|
||||
"tsup": "^8.3.6",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.0.11",
|
||||
"vitest": "^3.0.4"
|
||||
"vite": "^6.1.0",
|
||||
"vitest": "^3.0.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tailwindcss": "^3.4.17"
|
||||
@ -87,4 +88,4 @@
|
||||
"is-core-module": "npm:@nolyfill/is-core-module@^1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
838
pnpm-lock.yaml
generated
838
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -4,32 +4,59 @@ 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",
|
||||
@ -39,14 +66,21 @@ const supportedTags = [
|
||||
"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) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
// 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-attr/index.module.css
|
||||
"// 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"
|
||||
|
@ -3,7 +3,7 @@ import { describe, it } from "vitest";
|
||||
import { getBuild, matchSnapshot } from "./utils";
|
||||
|
||||
describe("emit", () => {
|
||||
const compileESBuild = getBuild("attr");
|
||||
const compileESBuild = getBuild("emit");
|
||||
|
||||
it("supports emitting as CSS module", async () => {
|
||||
const { files } = await compileESBuild({
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getBuild } from "./utils";
|
||||
|
||||
describe("babel-tailwind", () => {
|
||||
|
@ -45,8 +45,50 @@ 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("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))'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -2,6 +2,7 @@ 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 type { Scope } from "@babel/traverse";
|
||||
import hash from "@emotion/hash";
|
||||
import invariant from "tiny-invariant";
|
||||
|
||||
@ -17,6 +18,14 @@ type Type = "css" | "js";
|
||||
|
||||
export type BabelPluginUtils = ReturnType<typeof getUtils>;
|
||||
|
||||
interface Import {
|
||||
source: string;
|
||||
specifiers: {
|
||||
local: string;
|
||||
imported: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
function getUtils({
|
||||
path,
|
||||
state,
|
||||
@ -47,6 +56,36 @@ function getUtils({
|
||||
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 getStyleImport() {
|
||||
styleImport ??= path.scope.generateUidIdentifier("styles");
|
||||
return t.cloneNode(styleImport);
|
||||
@ -59,7 +98,16 @@ function getUtils({
|
||||
return t.cloneNode(cssModuleImport);
|
||||
};
|
||||
|
||||
const reuseImport = (scope: Scope, id?: string) => {
|
||||
if (id && scope.getBinding(id) === path.scope.getBinding(id)) {
|
||||
return t.identifier(id);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
program: path,
|
||||
existingCx,
|
||||
|
||||
getClass(type: Type, value: string) {
|
||||
return type === "css" ? getClass(value) : "tw_" + hash(value);
|
||||
},
|
||||
@ -74,7 +122,7 @@ function getUtils({
|
||||
.join("\n"),
|
||||
}),
|
||||
|
||||
recordIfAbsent(type: "css", entry: StyleMapEntry) {
|
||||
recordIfAbsent(type: Type, entry: StyleMapEntry) {
|
||||
const map = type === "css" ? cssMap : jsMap;
|
||||
if (!map.has(entry.key)) {
|
||||
map.set(entry.key, entry);
|
||||
@ -100,8 +148,11 @@ function getUtils({
|
||||
}
|
||||
},
|
||||
|
||||
getCx: () => {
|
||||
getCx: (localScope: Scope) => {
|
||||
if (cx == null) {
|
||||
const reuse = reuseImport(localScope, existingCx);
|
||||
if (reuse) return reuse;
|
||||
|
||||
cx = path.scope.generateUidIdentifier("cx");
|
||||
path.node.body.unshift(getClsxImport(t, cx, clsx));
|
||||
}
|
||||
@ -295,8 +346,8 @@ export function babelTailwind(
|
||||
|
||||
if (classNameAttribute) {
|
||||
const attrValue = classNameAttribute.value!;
|
||||
const wrap = (originalValue: b.types.Expression) =>
|
||||
t.callExpression(_.getCx(), [originalValue, valuePathNode]);
|
||||
const wrap = (...originalValue: (b.types.Expression | b.types.SpreadElement)[]) =>
|
||||
t.callExpression(_.getCx(path.scope), [valuePathNode, ...originalValue]);
|
||||
|
||||
// If both are string literals, we can merge them directly here
|
||||
if (t.isStringLiteral(attrValue) && t.isStringLiteral(valuePathNode)) {
|
||||
@ -309,13 +360,32 @@ export function babelTailwind(
|
||||
!t.isBlockStatement(internalAttrValue.body)
|
||||
) {
|
||||
internalAttrValue.body = wrap(internalAttrValue.body);
|
||||
} else if (
|
||||
// if the existing className is already wrapped with cx, we unwrap it
|
||||
// to avoid double calling: cx(cx())
|
||||
t.isCallExpression(internalAttrValue) &&
|
||||
t.isIdentifier(internalAttrValue.callee) &&
|
||||
_.existingCx &&
|
||||
_.program.scope
|
||||
.getBinding(_.existingCx)
|
||||
?.referencePaths.map(p => p.node)
|
||||
.includes(internalAttrValue.callee)
|
||||
) {
|
||||
classNameAttribute.value = t.jsxExpressionContainer(
|
||||
wrap(
|
||||
...(internalAttrValue.arguments as (
|
||||
| b.types.Expression
|
||||
| b.types.SpreadElement
|
||||
)[])
|
||||
)
|
||||
);
|
||||
} else {
|
||||
classNameAttribute.value = t.jsxExpressionContainer(wrap(internalAttrValue));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const wrap = (originalValue: b.types.Expression) =>
|
||||
t.callExpression(_.getCx(), [valuePathNode, originalValue]);
|
||||
t.callExpression(_.getCx(path.scope), [valuePathNode, originalValue]);
|
||||
|
||||
const rest = parent.attributes.filter(attr => t.isJSXSpreadAttribute(attr));
|
||||
let arg;
|
||||
@ -368,7 +438,7 @@ export function babelTailwind(
|
||||
} else {
|
||||
const containerValue = t.isStringLiteral(valuePathNode)
|
||||
? valuePathNode
|
||||
: t.callExpression(_.getCx(), [valuePathNode]);
|
||||
: t.callExpression(_.getCx(path.scope), [valuePathNode]);
|
||||
|
||||
parent.attributes.push(
|
||||
t.jsxAttribute(
|
||||
@ -400,7 +470,10 @@ function getClsxImport(t: BabelTypes, cx: t.Identifier, clsx: string) {
|
||||
t.stringLiteral("@emotion/css")
|
||||
);
|
||||
case "clsx":
|
||||
return t.importDeclaration([t.importDefaultSpecifier(cx)], t.stringLiteral("clsx"));
|
||||
return t.importDeclaration(
|
||||
[t.importSpecifier(cx, t.identifier("clsx"))],
|
||||
t.stringLiteral("clsx")
|
||||
);
|
||||
case "classnames":
|
||||
return t.importDeclaration(
|
||||
[t.importDefaultSpecifier(cx)],
|
||||
|
@ -164,32 +164,59 @@ const isNodePath = <T extends t.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",
|
||||
@ -199,12 +226,19 @@ const supportedTags = new Set<SupportedTag>([
|
||||
"sub",
|
||||
"summary",
|
||||
"sup",
|
||||
"svg",
|
||||
"table",
|
||||
"tbody",
|
||||
"td",
|
||||
"textarea",
|
||||
"tfoot",
|
||||
"th",
|
||||
"thead",
|
||||
"time",
|
||||
"tr",
|
||||
"u",
|
||||
"ul",
|
||||
"var",
|
||||
"video",
|
||||
"wbr",
|
||||
]);
|
||||
|
36
src/macro.d.ts
vendored
36
src/macro.d.ts
vendored
@ -13,32 +13,59 @@ 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"
|
||||
@ -48,14 +75,21 @@ export type SupportedTag =
|
||||
| "sub"
|
||||
| "summary"
|
||||
| "sup"
|
||||
| "svg"
|
||||
| "table"
|
||||
| "tbody"
|
||||
| "td"
|
||||
| "textarea"
|
||||
| "tfoot"
|
||||
| "th"
|
||||
| "thead"
|
||||
| "time"
|
||||
| "tr"
|
||||
| "u"
|
||||
| "ul"
|
||||
| "var"
|
||||
| "video";
|
||||
| "video"
|
||||
| "wbr";
|
||||
|
||||
type Modifier =
|
||||
| "2xl"
|
||||
|
Loading…
x
Reference in New Issue
Block a user