This commit is contained in:
Alex
2024-04-06 15:28:43 -04:00
parent dbfdf67983
commit 1109ff0dea
7 changed files with 256 additions and 146 deletions

View File

@ -13,6 +13,10 @@ const PLUGIN_NAME = "tailwind";
const ESBUILD_NAMESPACE = "babel-tailwind";
const ROLLUP_PREFIX = "\0tailwind:";
const { name } = [require][0](
process.env.BABEL_TAILWIND_BUILD ? "./package.json" : "../package.json"
);
const definePlugin =
<T>(fn: (runtime: typeof babel) => babel.Visitor<babel.PluginPass & T>) =>
(runtime: typeof babel) => {
@ -34,12 +38,25 @@ function matchPath(
fn?.(nodePath);
}
/**
* Tagged template macro function for Tailwind classes
* @example "tw" => tw`p-2 text-center`
*/
export type TaggedTailwindFunction = (strings: TemplateStringsArray) => string;
const tailwindDirectives = ["components", "utilities", "variants"] as const;
export interface TailwindPluginOptions {
/**
* Tailwind CSS configuration
*/
tailwindConfig: Omit<Config, "content">;
/**
* Directives to prefix to all Tailwind stylesheets
*/
directives?: (typeof tailwindDirectives)[number][] | "all";
/**
* Additional PostCSS plugins (optional)
*/
@ -51,6 +68,15 @@ export interface TailwindPluginOptions {
*/
jsxAttributeName?: string;
/**
* Tagged template macro function to use for Tailwind classes
* @default "tw"
* @example
* declare const tw: TaggedTailwindFunction;
* "tw" => tw`p-2 text-center`
*/
taggedTemplateName?: string;
/**
* The prefix to use for the generated class names.
* @default className => `tw-${hash(className)}`
@ -71,50 +97,51 @@ type GetClassName = (className: string) => string;
*/
export const getClassName: GetClassName = cls => "tw-" + hash(cls);
const babelTailwind = (
interface BabelPluginState {
getCx: () => t.Identifier;
tailwindMap: Map<string, string>;
}
function babelTailwind(
styleMap: Map<string, string>,
{
clsx,
getClassName: getClass = getClassName,
taggedTemplateName,
jsxAttributeName = "css",
}: TailwindPluginOptions
) =>
definePlugin<{
getCx: () => t.Identifier;
tailwindMap: Map<string, string>;
}>(({ types: t }) => ({
) {
function getClsxImport(t: typeof babel.types, cx: t.Identifier) {
switch (clsx) {
case "emotion":
return t.importDeclaration(
[t.importSpecifier(cx, t.identifier("cx"))],
t.stringLiteral("@emotion/css")
);
case "clsx":
return t.importDeclaration(
[t.importDefaultSpecifier(cx)],
t.stringLiteral("clsx")
);
case "classnames":
return t.importDeclaration(
[t.importDefaultSpecifier(cx)],
t.stringLiteral("classnames")
);
default:
throw new Error("Unknown clsx library");
}
}
return definePlugin<BabelPluginState>(({ types: t }) => ({
Program: {
enter(path, state) {
let cx: t.Identifier;
state.tailwindMap = new Map();
function getClsxImport() {
switch (clsx) {
case "emotion":
return t.importDeclaration(
[t.importSpecifier(cx, t.identifier("cx"))],
t.stringLiteral("@emotion/css")
);
case "clsx":
return t.importDeclaration(
[t.importDefaultSpecifier(cx)],
t.stringLiteral("clsx")
);
case "classnames":
return t.importDeclaration(
[t.importDefaultSpecifier(cx)],
t.stringLiteral("classnames")
);
default:
throw new Error("Unknown clsx library");
}
}
state.getCx = () => {
if (cx == null) {
cx = path.scope.generateUidIdentifier("cx");
path.node.body.unshift(getClsxImport());
path.node.body.unshift(getClsxImport(t, cx));
}
return t.cloneNode(cx);
};
@ -140,14 +167,16 @@ const babelTailwind = (
},
TaggedTemplateExpression(path, { tailwindMap }) {
if (taggedTemplateName == null) return;
const {
tag,
quasi: { quasis, expressions },
} = path.node;
if (!t.isIdentifier(tag, { name: "tw" })) return;
if (!t.isIdentifier(tag, { name: taggedTemplateName })) return;
if (expressions.length) {
throw new Error("tw`` should not contain expressions");
throw new Error(`${taggedTemplateName}\`\` should not contain expressions`);
}
const value = quasis[0].value.cooked;
@ -220,6 +249,7 @@ const babelTailwind = (
path.remove();
},
}));
}
/**
* An esbuild plugin that processes files with Babel if `getPlugins` returns any plugins.
@ -264,7 +294,12 @@ export const babelPlugin = ({
type Compile = ReturnType<typeof createPostCSS>;
function createPostCSS({ tailwindConfig, postCSSPlugins = [] }: TailwindPluginOptions) {
/** @internal */
export function createPostCSS({
tailwindConfig,
postCSSPlugins = [],
directives,
}: Pick<TailwindPluginOptions, "tailwindConfig" | "postCSSPlugins" | "directives">) {
const post = postcss([
tailwind({
...tailwindConfig,
@ -272,7 +307,11 @@ function createPostCSS({ tailwindConfig, postCSSPlugins = [] }: TailwindPluginOp
}),
...postCSSPlugins,
]);
return (css: string) => post.process(css, { from: undefined });
const appliedDirectives = directives === "all" ? tailwindDirectives : directives;
const directiveTexts = appliedDirectives?.map(d => `@tailwind ${d};\n`).join("") ?? "";
return (css: string) => post.process(directiveTexts + css, { from: undefined });
}
const esbuildPlugin = (
@ -292,7 +331,19 @@ const esbuildPlugin = (
}
});
build.onResolve({ filter: RegExp(`^${name}/base$`) }, () => ({
path: "directive:babel",
namespace: ESBUILD_NAMESPACE,
}));
build.onLoad({ filter: /.*/, namespace: ESBUILD_NAMESPACE }, async ({ path }) => {
if (path === "directive:babel") {
return {
contents: (await compile(`@tailwind base;`)).css!,
loader: "css",
};
}
if (!styleMap.has(path)) return;
const result = await compile(styleMap.get(path)!);