164 lines
4.2 KiB
TypeScript
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();
|
|
},
|
|
};
|
|
}
|