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: