import hash from "@emotion/hash"; import type { Config } from "tailwindcss"; import type { SetRequired } from "type-fest"; import type postcss from "postcss"; import { memoize, without } from "lodash"; import { type ClassNameCollector, babelTailwind } from "./babel-tailwind"; import { esbuildPlugin } from "./esbuild-postcss"; import { vitePlugin } from "./vite-plugin"; import { type StyleMap, createPostCSS } from "./shared"; import { convertCssToJS } from "./css-to-js"; export { babelPlugin } from "./esbuild-babel"; export { createPostCSS } from "./shared"; type GetClassName = (className: string) => string; export type BuildStyleFile = ( path: string ) => Promise; export interface TailwindPluginOptions { /** * Tailwind CSS configuration */ tailwindConfig?: Omit; /** * Prefix to all Tailwind stylesheets * * @example * tailwind base; * tailwind components; * tailwind utilities; */ prefix?: string; /** * Additional PostCSS plugins (optional) */ postCSSPlugins?: postcss.AcceptedPlugin[]; /** * 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"; /** * @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; } export type ResolveTailwindOptions = SetRequired< TailwindPluginOptions, | "clsx" | "jsxAttributeAction" | "jsxAttributeName" | "postCSSPlugins" | "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 resolvedOptions: ResolveTailwindOptions = { getClassName, jsxAttributeAction: "delete", jsxAttributeName: "css", postCSSPlugins: [], styleMap: new Map(), tailwindConfig: {}, ...options, }; const getCompiler = () => createPostCSS(resolvedOptions); const { styleMap } = resolvedOptions; const compile = options.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} {\n /* @apply ${tw} */\n @apply ${tw}\n}`; }) .join("\n") ); if (path.endsWith(".css")) { return ["css", compiled] as const; } else if (path.endsWith(".js")) { const js = convertCssToJS(compiled, x => x.slice(1)); return ["js", js] as const; } else { throw new Error("Unknown file extension"); } }; return { compile, babel: (onCollect?: ClassNameCollector) => babelTailwind(resolvedOptions, onCollect), esbuild: () => esbuildPlugin({ styleMap, compile, buildStyleFile }), /** Requires `options.vite` to be `true`. */ vite: () => vitePlugin({ styleMap, compile, buildStyleFile }), styleMap, options, getCompiler, [Symbol.dispose]() { styleMap.clear(); }, }; }