import { createRequire } from "node:module"; import hash from "@emotion/hash"; import type { BabelOptions, Options as ReactOptions } from "@vitejs/plugin-react"; import { transformSync } from "esbuild"; import { memoize, without } from "lodash-es"; import type { Config } from "tailwindcss"; import type { SetRequired } from "type-fest"; import { type ClassNameCollector, babelTailwind } from "./babel/index"; import { toJSCode } from "./css-to-js"; import { esbuildPlugin } from "./esbuild-postcss"; import { type StyleMap, createPostCSS } from "./shared"; import { vitePlugin } from "./vite-plugin"; export { isMacrosName } from "./vite-plugin"; export { babelPlugin } from "./esbuild-babel"; export { createPostCSS } from "./shared"; type GetClassName = (className: string) => string; export type BuildStyleFile = ( path: string ) => Promise; const require = createRequire(import.meta.url); export interface TailwindPluginOptions { /** * Tailwind CSS configuration */ tailwindConfig?: Omit; /** * Prefix to all Tailwind stylesheets * * @example * tailwind base; * tailwind components; * tailwind utilities; */ prefix?: string; /** * Attribute to use for tailwind classes in JSX * @default "css" */ jsxAttributeName?: string; /** * What to do with the original attribute after processing * @default "delete" */ jsxAttributeAction?: "delete" | "preserve" | ["rename", string]; /** * The prefix to use for the generated class names. * @default className => `tw-${hash(className)}` */ getClassName?: GetClassName; /** * Preferred library for classnames */ clsx: "clsx" | "classnames" | "emotion"; /** * Use react-aria-component’s `composeRenderProps` function. */ composeRenderProps?: boolean; /** * @internal */ styleMap?: StyleMap; /** * Custom CSS compile function * @example * async css => (await postcss.process(css, { plugins: [tailwindcss()] })).css */ compile?(css: string): Promise; /** * Using the vite plugin? */ vite?: boolean; /** * Keep the original classnames in the CSS output */ addSourceAsComment?: boolean; /** * Emit as CSS modules */ cssModules?: boolean; /** * Emit type. `css-import` for plain CSS import, * `css-module` for CSS modules, `css-in-js` for JS. */ // emitType: "css-import" | "css-module" | "css-in-js"; } export type ResolveTailwindOptions = SetRequired< TailwindPluginOptions, "clsx" | "jsxAttributeAction" | "jsxAttributeName" | "styleMap" | "tailwindConfig" >; /** * Hashes and prefixes a string of Tailwind class names. * @example getClassName("p-2 text-center") // "tw-1r6fxxz" */ export const getClassName: GetClassName = cls => "tw-" + hash(cls); /** * Main entry. Returns the plugins and utilities for processing Tailwind * classNames in JS. * * @example * import { build } from "esbuild"; * import getTailwindPlugins, { babelPlugin } from "babel-tailwind"; * * const tailwind = getTailwindPlugins(options); * const result = await build({ * plugins: [ * babelPlugin({ getPlugins: () => [tailwind.babel] }), * tailwind.esbuild, * ], * }); */ export function getTailwindPlugins(options: TailwindPluginOptions) { const { addSourceAsComment, compile: _compile, cssModules } = options; const resolvedOptions: ResolveTailwindOptions = { getClassName, jsxAttributeAction: "delete", jsxAttributeName: "css", styleMap: new Map(), tailwindConfig: {}, ...options, }; const getCompiler = () => createPostCSS(resolvedOptions); const { styleMap } = resolvedOptions; const compile = _compile ?? memoize(getCompiler()); const buildStyleFile: BuildStyleFile = async path => { const styles = styleMap.get(path)!; const compiled = await compile( styles .map(({ classNames, key }) => { const tw = without(classNames, "group").join(" "); return [ `.${key} {`, addSourceAsComment && ` /* @preserve ${tw} */`, ` @apply ${tw};`, "}", ] .filter(Boolean) .join("\n"); }) .join("\n") ); if (path.endsWith(".css")) { 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; } else { throw new Error("Unknown file extension"); } }; const babel = (onCollect?: ClassNameCollector) => babelTailwind(resolvedOptions, onCollect); const patchBabelOptions = (options: BabelOptions) => { (options.plugins ??= []).push(babel()); }; return { compile, babel: (onCollect?: ClassNameCollector) => babelTailwind(resolvedOptions, onCollect), esbuild: () => esbuildPlugin({ styleMap, compile, buildStyleFile }), vite: () => { resolvedOptions.vite = true; return vitePlugin({ styleMap, compile, buildStyleFile }); }, react(options: ReactOptions = {}) { const reactModule = require("@vitejs/plugin-react"); const reactPlugin: typeof import("@vitejs/plugin-react").default = "default" in reactModule ? reactModule.default : reactModule; options.babel ??= {}; if (typeof options.babel === "function") { const fn = options.babel; options.babel = (id, options) => { const result = fn(id, options); patchBabelOptions(result); return result; }; } else { patchBabelOptions(options.babel); } return reactPlugin(options); }, styleMap, options, getCompiler, buildStyleFile, [Symbol.dispose]() { styleMap.clear(); }, }; }