Improve error handling
This commit is contained in:
315
src/index.ts
315
src/index.ts
@ -1,42 +1,15 @@
|
||||
import { basename, dirname, extname, join } from "node:path";
|
||||
import hash from "@emotion/hash";
|
||||
import type babel from "@babel/core";
|
||||
import type * as vite from "vite";
|
||||
import type * as esbuild from "esbuild";
|
||||
import { type NodePath, type types as t } from "@babel/core";
|
||||
import tailwind, { type Config } from "tailwindcss";
|
||||
import postcss from "postcss";
|
||||
import type { Config } from "tailwindcss";
|
||||
import type postcss from "postcss";
|
||||
import { babelTailwind } from "./babel-tailwind";
|
||||
import { esbuildPlugin } from "./esbuild-postcss";
|
||||
import { vitePlugin } from "./vite-plugin";
|
||||
import { type StyleMap, createPostCSS, type tailwindDirectives } from "./shared";
|
||||
|
||||
export { babelPlugin } from "./esbuild-babel";
|
||||
export { createPostCSS } from "./shared";
|
||||
|
||||
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) => {
|
||||
const plugin: babel.PluginObj<babel.PluginPass & T> = {
|
||||
visitor: fn(runtime),
|
||||
};
|
||||
return plugin as babel.PluginObj;
|
||||
};
|
||||
|
||||
const extractJSXContainer = (attr: NonNullable<t.JSXAttribute["value"]>): t.Expression =>
|
||||
attr.type === "JSXExpressionContainer" ? (attr.expression as t.Expression) : attr;
|
||||
|
||||
function matchPath(
|
||||
nodePath: NodePath<t.Node | null | undefined>,
|
||||
fns: (dig: (nodePath: NodePath) => void) => babel.Visitor
|
||||
) {
|
||||
if (!nodePath.node) return;
|
||||
const fn = fns(path => matchPath(path, fns))[nodePath.node.type] as any;
|
||||
fn?.(nodePath);
|
||||
}
|
||||
type GetClassName = (className: string) => string;
|
||||
|
||||
/**
|
||||
* Tagged template macro function for Tailwind classes
|
||||
@ -44,8 +17,6 @@ function matchPath(
|
||||
*/
|
||||
export type TaggedTailwindFunction = (strings: TemplateStringsArray) => string;
|
||||
|
||||
const tailwindDirectives = ["components", "utilities", "variants"] as const;
|
||||
|
||||
export interface TailwindPluginOptions {
|
||||
/**
|
||||
* Tailwind CSS configuration
|
||||
@ -95,280 +66,12 @@ export interface TailwindPluginOptions {
|
||||
clsx: "clsx" | "classnames" | "emotion";
|
||||
}
|
||||
|
||||
type GetClassName = (className: string) => string;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
interface BabelPluginState {
|
||||
getCx: () => t.Identifier;
|
||||
tailwindMap: Map<string, string>;
|
||||
}
|
||||
|
||||
function babelTailwind(
|
||||
styleMap: Map<string, string>,
|
||||
{
|
||||
clsx,
|
||||
getClassName: getClass = getClassName,
|
||||
taggedTemplateName,
|
||||
jsxAttributeAction = "delete",
|
||||
jsxAttributeName = "css",
|
||||
}: TailwindPluginOptions
|
||||
) {
|
||||
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();
|
||||
state.getCx = () => {
|
||||
if (cx == null) {
|
||||
cx = path.scope.generateUidIdentifier("cx");
|
||||
path.node.body.unshift(getClsxImport(t, cx));
|
||||
}
|
||||
return t.cloneNode(cx);
|
||||
};
|
||||
},
|
||||
|
||||
exit({ node }, { filename, tailwindMap }) {
|
||||
if (!tailwindMap.size) return;
|
||||
if (!filename) {
|
||||
throw new Error("babel: missing state.filename");
|
||||
}
|
||||
|
||||
const cssName = basename(filename, extname(filename)) + ".css";
|
||||
node.body.unshift(
|
||||
t.importDeclaration([], t.stringLiteral(`tailwind:./${cssName}`))
|
||||
);
|
||||
|
||||
const result = Array.from(tailwindMap)
|
||||
.map(([className, value]) => `.${className} {\n @apply ${value}\n}`)
|
||||
.join("\n");
|
||||
|
||||
styleMap.set(join(dirname(filename), cssName), result);
|
||||
},
|
||||
},
|
||||
|
||||
TaggedTemplateExpression(path, { tailwindMap }) {
|
||||
if (taggedTemplateName == null) return;
|
||||
|
||||
const {
|
||||
tag,
|
||||
quasi: { quasis, expressions },
|
||||
} = path.node;
|
||||
if (!t.isIdentifier(tag, { name: taggedTemplateName })) return;
|
||||
|
||||
if (expressions.length) {
|
||||
throw new Error(`${taggedTemplateName}\`\` should not contain expressions`);
|
||||
}
|
||||
|
||||
const value = quasis[0].value.cooked;
|
||||
if (value) {
|
||||
const trimmed = value.replace(/\s+/g, " ").trim();
|
||||
const className = getClass(trimmed);
|
||||
tailwindMap.set(className, trimmed);
|
||||
path.replaceWith(t.stringLiteral(className));
|
||||
}
|
||||
},
|
||||
|
||||
JSXAttribute(path, { tailwindMap, getCx }) {
|
||||
const { name } = path.node;
|
||||
if (name.name !== jsxAttributeName) return;
|
||||
|
||||
const valuePath = path.get("value");
|
||||
if (!valuePath.node) return;
|
||||
|
||||
const copy =
|
||||
jsxAttributeAction === "delete" ? undefined : t.cloneNode(valuePath.node, true);
|
||||
|
||||
const parent = path.parent as t.JSXOpeningElement;
|
||||
const classNameAttribute = parent.attributes.find(
|
||||
(attr): attr is t.JSXAttribute =>
|
||||
t.isJSXAttribute(attr) && attr.name.name === "className"
|
||||
);
|
||||
|
||||
matchPath(valuePath, go => ({
|
||||
StringLiteral(path) {
|
||||
const { value } = path.node;
|
||||
if (value) {
|
||||
const trimmed = value.replace(/\s+/g, " ").trim();
|
||||
const className = getClass(trimmed);
|
||||
tailwindMap.set(className, trimmed);
|
||||
path.replaceWith(t.stringLiteral(className));
|
||||
}
|
||||
},
|
||||
JSXExpressionContainer(path) {
|
||||
go(path.get("expression"));
|
||||
},
|
||||
ConditionalExpression(path) {
|
||||
go(path.get("consequent"));
|
||||
go(path.get("alternate"));
|
||||
},
|
||||
LogicalExpression(path) {
|
||||
go(path.get("right"));
|
||||
},
|
||||
CallExpression(path) {
|
||||
for (const arg of path.get("arguments")) {
|
||||
go(arg);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
if (classNameAttribute) {
|
||||
const attrValue = classNameAttribute.value!;
|
||||
|
||||
// If both are string literals, we can merge them directly here
|
||||
if (t.isStringLiteral(attrValue) && t.isStringLiteral(valuePath.node)) {
|
||||
attrValue.value +=
|
||||
(attrValue.value.at(-1) === " " ? "" : " ") + valuePath.node.value;
|
||||
} else {
|
||||
classNameAttribute.value = t.jsxExpressionContainer(
|
||||
t.callExpression(getCx(), [
|
||||
extractJSXContainer(attrValue),
|
||||
extractJSXContainer(valuePath.node),
|
||||
])
|
||||
);
|
||||
}
|
||||
} else {
|
||||
parent.attributes.push(
|
||||
t.jsxAttribute(t.jsxIdentifier("className"), valuePath.node)
|
||||
);
|
||||
}
|
||||
|
||||
if (jsxAttributeAction === "delete") {
|
||||
path.remove();
|
||||
} else {
|
||||
path.node.value = copy!;
|
||||
if (Array.isArray(jsxAttributeAction) && jsxAttributeAction[0] === "rename") {
|
||||
path.node.name.name = jsxAttributeAction[1];
|
||||
}
|
||||
}
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
type Compile = ReturnType<typeof createPostCSS>;
|
||||
|
||||
/** @internal */
|
||||
export function createPostCSS({
|
||||
tailwindConfig,
|
||||
postCSSPlugins = [],
|
||||
directives,
|
||||
}: Pick<TailwindPluginOptions, "tailwindConfig" | "postCSSPlugins" | "directives">) {
|
||||
const post = postcss([
|
||||
tailwind({
|
||||
...tailwindConfig,
|
||||
content: [{ raw: "<br>", extension: "html" }],
|
||||
}),
|
||||
...postCSSPlugins,
|
||||
]);
|
||||
|
||||
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 = (
|
||||
styleMap: Map<string, string>,
|
||||
compile: Compile
|
||||
): esbuild.Plugin => ({
|
||||
name: PLUGIN_NAME,
|
||||
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^tailwind:.+\.css$/ }, ({ path, importer }) => {
|
||||
const resolved = join(dirname(importer), path.replace(/^tailwind:/, ""));
|
||||
if (styleMap.has(resolved)) {
|
||||
return {
|
||||
path: resolved,
|
||||
namespace: ESBUILD_NAMESPACE,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
build.onResolve({ filter: RegExp(`^${name}/base$`) }, () => ({
|
||||
path: "directive:base",
|
||||
namespace: ESBUILD_NAMESPACE,
|
||||
}));
|
||||
|
||||
build.onLoad({ filter: /.*/, namespace: ESBUILD_NAMESPACE }, async ({ path }) => {
|
||||
if (path === "directive:base") {
|
||||
return {
|
||||
contents: (await compile(`@tailwind base;`)).css,
|
||||
loader: "css",
|
||||
};
|
||||
}
|
||||
|
||||
if (!styleMap.has(path)) return;
|
||||
const result = await compile(styleMap.get(path)!);
|
||||
|
||||
return {
|
||||
contents: result.css,
|
||||
loader: "css",
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const vitePlugin = (styleMap: Map<string, string>, compile: Compile): vite.Plugin => ({
|
||||
name: "tailwind",
|
||||
resolveId(id, importer) {
|
||||
if (id === `${name}/base`) {
|
||||
return {
|
||||
id: ROLLUP_PREFIX + "directive:base",
|
||||
moduleSideEffects: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (id.startsWith("tailwind:")) {
|
||||
const resolved = join(dirname(importer!), id.slice("tailwind:".length));
|
||||
if (styleMap.has(resolved)) {
|
||||
return {
|
||||
id: ROLLUP_PREFIX + resolved,
|
||||
moduleSideEffects: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
async load(id: string) {
|
||||
if (id.startsWith(ROLLUP_PREFIX)) {
|
||||
const resolved = id.slice(ROLLUP_PREFIX.length);
|
||||
if (resolved === "directive:base") {
|
||||
return (await compile("@tailwind base;")).css;
|
||||
}
|
||||
|
||||
if (styleMap.has(resolved)) {
|
||||
const result = await compile(styleMap.get(resolved)!);
|
||||
return result.css;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Main entry. Returns the plugins and utilities for processing Tailwind
|
||||
* classNames in JS.
|
||||
@ -386,7 +89,7 @@ const vitePlugin = (styleMap: Map<string, string>, compile: Compile): vite.Plugi
|
||||
* });
|
||||
*/
|
||||
export function getTailwindPlugins(options: TailwindPluginOptions) {
|
||||
const styleMap = new Map<string, string>();
|
||||
const styleMap: StyleMap = new Map();
|
||||
const compile = createPostCSS(options);
|
||||
|
||||
return {
|
||||
|
Reference in New Issue
Block a user