222 lines
5.8 KiB
TypeScript
222 lines
5.8 KiB
TypeScript
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<readonly ["css" | "local-css", string] | readonly ["js", string]>;
|
||
|
||
const require = createRequire(import.meta.url);
|
||
|
||
export interface TailwindPluginOptions {
|
||
/**
|
||
* Tailwind CSS configuration
|
||
*/
|
||
tailwindConfig?: Omit<Config, "content">;
|
||
|
||
/**
|
||
* 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<string>;
|
||
|
||
/**
|
||
* 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();
|
||
},
|
||
};
|
||
}
|