babel-tailwind/src/index.ts
2024-07-27 02:55:54 -04:00

164 lines
4.2 KiB
TypeScript

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<readonly ["css", string] | readonly ["js", string]>;
export interface TailwindPluginOptions {
/**
* Tailwind CSS configuration
*/
tailwindConfig?: Omit<Config, "content">;
/**
* 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<string>;
/**
* 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();
},
};
}