From 1f5e7fa049feeeb92b4946a1a3de33fd9ed52bf2 Mon Sep 17 00:00:00 2001
From: Alex <8125011+alex-kinokon@users.noreply.github.com>
Date: Mon, 3 Feb 2025 00:05:24 -0500
Subject: [PATCH] Fix issue
---
package.json | 2 +-
src/__tests__/__snapshots__/emit.test.ts.snap | 27 +++++++++++
src/__tests__/attr.test.ts | 3 +-
src/__tests__/base.test.ts | 2 +
src/__tests__/emit.test.ts | 26 ++++++++++
src/__tests__/merge.test.ts | 2 +
src/__tests__/options.test.ts | 1 +
src/__tests__/styleObject.test.ts | 1 +
src/__tests__/tw.test.ts | 1 +
src/__tests__/utils.ts | 6 ++-
src/babel/index.ts | 47 ++++++++++++++++---
src/esbuild-babel.ts | 5 +-
src/index.ts | 21 +++++++--
src/vendor/animate.ts | 13 +++--
14 files changed, 135 insertions(+), 22 deletions(-)
create mode 100644 src/__tests__/__snapshots__/emit.test.ts.snap
create mode 100644 src/__tests__/emit.test.ts
diff --git a/package.json b/package.json
index e894436..84042b4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@aet/tailwind",
- "version": "1.0.20",
+ "version": "1.0.23",
"license": "MIT",
"type": "module",
"scripts": {
diff --git a/src/__tests__/__snapshots__/emit.test.ts.snap b/src/__tests__/__snapshots__/emit.test.ts.snap
new file mode 100644
index 0000000..7a4cede
--- /dev/null
+++ b/src/__tests__/__snapshots__/emit.test.ts.snap
@@ -0,0 +1,27 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`emit > supports emitting as CSS module 1`] = `
+"// babel-tailwind:/Users/aet/Documents/Git/babel-tailwind/src/.temp-attr/index.module.css
+var index_default = {
+ "tw-gqn2k6": "index_tw-gqn2k6",
+ "tw-1qtvvjy": "index_tw-1qtvvjy"
+};
+
+import { cx as _cx } from "@emotion/css";
+import { jsx } from "react/jsx-runtime";
+function Hello() {
+ return /* @__PURE__ */ jsx("div", { className: _cx([index_default["tw-gqn2k6"], index_default["tw-1qtvvjy"]]), children: "Hello, world!" });
+}
+export {
+ Hello
+};"
+`;
+
+exports[`emit > supports emitting as CSS module 2`] = `
+".index_tw-gqn2k6 {
+ text-align: center;
+}
+.index_tw-1qtvvjy:hover {
+ font-weight: 600;
+}"
+`;
diff --git a/src/__tests__/attr.test.ts b/src/__tests__/attr.test.ts
index d6e6e9f..cdd5bfa 100644
--- a/src/__tests__/attr.test.ts
+++ b/src/__tests__/attr.test.ts
@@ -1,4 +1,5 @@
import { describe, it } from "vitest";
+
import { getBuild, matchSnapshot } from "./utils";
describe("attr", () => {
@@ -61,7 +62,7 @@ describe("attr", () => {
matchSnapshot(files);
});
- it.only("fails", async () => {
+ it("fails", async () => {
const { files } = await compileESBuild({
clsx: "emotion",
expectFiles: 2,
diff --git a/src/__tests__/base.test.ts b/src/__tests__/base.test.ts
index b4f5d1d..294bb05 100644
--- a/src/__tests__/base.test.ts
+++ b/src/__tests__/base.test.ts
@@ -1,5 +1,7 @@
import { describe, expect, it } from "vitest";
+
import { createPostCSS } from "../index";
+
import { getBuild, minCSS, name } from "./utils";
describe("babel-tailwind", () => {
diff --git a/src/__tests__/emit.test.ts b/src/__tests__/emit.test.ts
new file mode 100644
index 0000000..589a804
--- /dev/null
+++ b/src/__tests__/emit.test.ts
@@ -0,0 +1,26 @@
+import { describe, it } from "vitest";
+
+import { getBuild, matchSnapshot } from "./utils";
+
+describe("emit", () => {
+ const compileESBuild = getBuild("attr");
+
+ it("supports emitting as CSS module", async () => {
+ const { files } = await compileESBuild({
+ clsx: "emotion",
+ expectFiles: 2,
+ cssModules: true,
+ javascript: /* tsx */ `
+ export function Hello() {
+ return (
+
+ Hello, world!
+
+ );
+ }
+ `,
+ });
+
+ matchSnapshot(files);
+ });
+});
diff --git a/src/__tests__/merge.test.ts b/src/__tests__/merge.test.ts
index 48bbb41..713c084 100644
--- a/src/__tests__/merge.test.ts
+++ b/src/__tests__/merge.test.ts
@@ -1,6 +1,8 @@
/* eslint-disable unicorn/string-content */
import { describe, expect, it } from "vitest";
+
import { getClassName } from "../index";
+
import { getBuild } from "./utils";
describe("merges with existing className attribute", () => {
diff --git a/src/__tests__/options.test.ts b/src/__tests__/options.test.ts
index 3958c6e..980d702 100644
--- a/src/__tests__/options.test.ts
+++ b/src/__tests__/options.test.ts
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
+
import { getBuild, matchSnapshot } from "./utils";
describe("options", () => {
diff --git a/src/__tests__/styleObject.test.ts b/src/__tests__/styleObject.test.ts
index 1f2d602..f6c0679 100644
--- a/src/__tests__/styleObject.test.ts
+++ b/src/__tests__/styleObject.test.ts
@@ -1,4 +1,5 @@
import { describe, it } from "vitest";
+
import { getBuild, matchSnapshot } from "./utils";
describe("babel-tailwind", () => {
diff --git a/src/__tests__/tw.test.ts b/src/__tests__/tw.test.ts
index 162eeb6..ca2adb7 100644
--- a/src/__tests__/tw.test.ts
+++ b/src/__tests__/tw.test.ts
@@ -1,4 +1,5 @@
import { describe, it } from "vitest";
+
import { getBuild, matchSnapshot } from "./utils";
describe("babel-tailwind", () => {
diff --git a/src/__tests__/utils.ts b/src/__tests__/utils.ts
index f534427..d4cc708 100644
--- a/src/__tests__/utils.ts
+++ b/src/__tests__/utils.ts
@@ -1,8 +1,10 @@
import { promises as fs } from "node:fs";
import { join, resolve } from "node:path";
-import { afterEach, beforeEach, expect } from "vitest";
-import * as esbuild from "esbuild";
+
import dedent from "dedent";
+import * as esbuild from "esbuild";
+import { afterEach, beforeEach, expect } from "vitest";
+
import { type TailwindPluginOptions, babelPlugin, getTailwindPlugins } from "../index";
export { name } from "../../package.json" with { type: "json" };
diff --git a/src/babel/index.ts b/src/babel/index.ts
index 3277e62..19a5a7d 100644
--- a/src/babel/index.ts
+++ b/src/babel/index.ts
@@ -35,12 +35,14 @@ function getUtils({
clsx,
getClassName: getClass = getClassName,
vite: bustCache,
+ cssModules,
} = options;
let cx: t.Identifier;
let tslibImport: t.Identifier;
let styleImport: t.Identifier;
let classedImport: t.Identifier;
+ let cssModuleImport: t.Identifier;
const cssMap = new Map();
const jsMap = new Map();
@@ -50,6 +52,13 @@ function getUtils({
return t.cloneNode(styleImport);
}
+ const getCssModuleImport = () => {
+ if (cssModuleImport == null) {
+ cssModuleImport = path.scope.generateUidIdentifier("cssModule");
+ }
+ return t.cloneNode(cssModuleImport);
+ };
+
return {
getClass(type: Type, value: string) {
return type === "css" ? getClass(value) : "tw_" + hash(value);
@@ -65,7 +74,7 @@ function getUtils({
.join("\n"),
}),
- recordIfAbsent(type: Type, entry: StyleMapEntry) {
+ recordIfAbsent(type: "css", entry: StyleMapEntry) {
const map = type === "css" ? cssMap : jsMap;
if (!map.has(entry.key)) {
map.set(entry.key, entry);
@@ -125,18 +134,44 @@ function getUtils({
return t.cloneNode(classedImport);
},
+ getCssModuleImport,
+
+ getClassNameValue: (className: string) => {
+ const validId = t.isValidIdentifier(className);
+ return cssModules
+ ? t.memberExpression(
+ getCssModuleImport(),
+ validId ? t.identifier(className) : t.stringLiteral(className),
+ !validId
+ )
+ : t.stringLiteral(className);
+ },
+
finish(node: t.Program) {
const { filename } = state;
if (!cssMap.size && !jsMap.size) return;
invariant(filename, "babel: missing state.filename");
if (cssMap.size) {
- const cssName = basename(filename, extname(filename)) + ".css";
+ const cssName =
+ basename(filename, extname(filename)) +
+ (cssModuleImport ? ".module" : "") +
+ ".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)));
+ if (cssModuleImport) {
+ const importee = `tailwind:./${cssName}` + getSuffix(bustCache, value);
+ node.body.unshift(
+ t.importDeclaration(
+ [t.importDefaultSpecifier(cssModuleImport)],
+ t.stringLiteral(importee)
+ )
+ );
+ } else {
+ const importee = `tailwind:./${cssName}` + getSuffix(bustCache, value);
+ node.body.unshift(t.importDeclaration([], t.stringLiteral(importee)));
+ }
styleMap.set(path, value);
onCollect?.(path, value);
@@ -212,7 +247,7 @@ export function babelTailwind(
classNames: trimmed,
location: _.sliceText(node),
});
- path.replaceWith(t.stringLiteral(className));
+ path.replaceWith(_.getClassNameValue(className));
}
},
ArrayExpression(path) {
@@ -228,7 +263,7 @@ export function babelTailwind(
classNames: trimmed,
location: _.sliceText(path.node),
});
- path.replaceWith(t.stringLiteral(className));
+ path.replaceWith(_.getClassNameValue(className));
},
JSXExpressionContainer(path) {
go(path.get("expression"));
diff --git a/src/esbuild-babel.ts b/src/esbuild-babel.ts
index dbe1eee..de43998 100644
--- a/src/esbuild-babel.ts
+++ b/src/esbuild-babel.ts
@@ -1,9 +1,10 @@
import { readFileSync } from "node:fs";
import { extname } from "node:path";
-import { once } from "lodash-es";
+
import type babel from "@babel/core";
-import type * as esbuild from "esbuild";
import { transformSync } from "@babel/core";
+import type * as esbuild from "esbuild";
+import { once } from "lodash-es";
/**
* An esbuild plugin that processes files with Babel if `plugins` is not empty.
diff --git a/src/index.ts b/src/index.ts
index 1f1d867..1e897e5 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -19,7 +19,7 @@ export { createPostCSS } from "./shared";
type GetClassName = (className: string) => string;
export type BuildStyleFile = (
path: string
-) => Promise;
+) => Promise;
export interface TailwindPluginOptions {
/**
@@ -86,6 +86,17 @@ export interface TailwindPluginOptions {
* Keep the original classnames in the CSS output
*/
addSourceAsComment?: boolean;
+
+ /**
+ * Emit as CSS modules
+ */
+ cssModules?: boolean;
+
+ /**
+ * Emit type. `css-import` for plain CSS import,
+ * `css-module` for CSS modules, `css-in-js` for JS.
+ */
+ // emitType: "css-import" | "css-module" | "css-in-js";
}
export type ResolveTailwindOptions = SetRequired<
@@ -121,7 +132,7 @@ export const getClassName: GetClassName = cls => "tw-" + hash(cls);
* });
*/
export function getTailwindPlugins(options: TailwindPluginOptions) {
- const { addSourceAsComment, compile: _compile } = options;
+ const { addSourceAsComment, compile: _compile, cssModules } = options;
const resolvedOptions: ResolveTailwindOptions = {
getClassName,
jsxAttributeAction: "delete",
@@ -155,7 +166,10 @@ export function getTailwindPlugins(options: TailwindPluginOptions) {
.join("\n")
);
if (path.endsWith(".css")) {
- return ["css", transformSync(compiled, { loader: "css" }).code] as const;
+ return [
+ cssModules ? "local-css" : "css",
+ transformSync(compiled, { loader: "css" }).code,
+ ] as const;
} else if (path.endsWith(".js")) {
const js = toJSCode(compiled, x => x.slice(1));
return ["js", js] as const;
@@ -176,6 +190,7 @@ export function getTailwindPlugins(options: TailwindPluginOptions) {
styleMap,
options,
getCompiler,
+ buildStyleFile,
[Symbol.dispose]() {
styleMap.clear();
},
diff --git a/src/vendor/animate.ts b/src/vendor/animate.ts
index d3900fd..13a0561 100644
--- a/src/vendor/animate.ts
+++ b/src/vendor/animate.ts
@@ -1,4 +1,5 @@
// https://github.com/jamiebuilds/tailwindcss-animate/commit/ac0dd3a3c81681b78f1d8ea5e7478044213995e1
+// https://github.com/tailwindlabs/tailwindcss/discussions/11164#discussioncomment-5819097
import plugin from "tailwindcss/plugin.js";
import type { PluginAPI } from "tailwindcss/types/config";
@@ -11,11 +12,7 @@ function filterDefault(values: T) {
export default plugin(
({ addUtilities, matchUtilities, theme }) => {
addUtilities({
- "@keyframes enter": theme("keyframes.enter"),
- "@keyframes exit": theme("keyframes.exit"),
".animate-in": {
- animationName: "enter",
- animationDuration: theme("animationDuration.DEFAULT"),
"--tw-enter-opacity": "initial",
"--tw-enter-scale": "initial",
"--tw-enter-rotate": "initial",
@@ -23,8 +20,6 @@ export default plugin(
"--tw-enter-translate-y": "initial",
},
".animate-out": {
- animationName: "exit",
- animationDuration: theme("animationDuration.DEFAULT"),
"--tw-exit-opacity": "initial",
"--tw-exit-scale": "initial",
"--tw-exit-rotate": "initial",
@@ -168,6 +163,10 @@ export default plugin(
1: "1",
infinite: "infinite",
},
+ animation: ({ theme }) => ({
+ out: `leave ${theme("animationDuration.DEFAULT")}`,
+ in: `enter ${theme("animationDuration.DEFAULT")}`,
+ }),
keyframes: {
enter: {
from: {
@@ -176,7 +175,7 @@ export default plugin(
"translate3d(var(--tw-enter-translate-x, 0), var(--tw-enter-translate-y, 0), 0) scale3d(var(--tw-enter-scale, 1), var(--tw-enter-scale, 1), var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))",
},
},
- exit: {
+ leave: {
to: {
opacity: "var(--tw-exit-opacity, 1)",
transform: