diff --git a/package.json b/package.json
index 82efab3..2d7f5c0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@aet/babel-tailwind",
- "version": "0.0.1-beta.4",
+ "version": "0.0.1-beta.5",
"main": "dist/index.js",
"license": "MIT",
"private": true,
@@ -12,7 +12,7 @@
"dist"
],
"devDependencies": {
- "@aet/eslint-rules": "^0.0.19",
+ "@aet/eslint-rules": "^0.0.21",
"@types/babel__core": "^7.20.5",
"@types/bun": "^1.0.12",
"@types/lodash": "^4.17.0",
@@ -43,4 +43,4 @@
"singleQuote": false,
"trailingComma": "es5"
}
-}
+}
\ No newline at end of file
diff --git a/src/esbuild-babel.ts b/src/esbuild-babel.ts
new file mode 100644
index 0000000..82bffda
--- /dev/null
+++ b/src/esbuild-babel.ts
@@ -0,0 +1,51 @@
+import { readFileSync } from "node:fs";
+import { extname } from "node:path";
+import { once } from "lodash";
+import type babel from "@babel/core";
+import type * as esbuild from "esbuild";
+import { transformSync } from "@babel/core";
+
+/**
+ * An esbuild plugin that processes files with Babel if `getPlugins` returns any plugins.
+ */
+export const babelPlugin = ({
+ filter = /\.[jt]sx?$/,
+ plugins: getPlugins,
+}: {
+ filter?: RegExp;
+ plugins:
+ | babel.PluginItem[]
+ | ((file: { path: string; contents: string }) => babel.PluginItem[]);
+}): esbuild.Plugin => ({
+ name: "babel-plugin",
+ setup(build) {
+ build.onLoad({ filter }, ({ path }) => {
+ const load = once(() => readFileSync(path, "utf-8"));
+ const plugins = Array.isArray(getPlugins)
+ ? getPlugins
+ : getPlugins({
+ path,
+ get contents() {
+ return load();
+ },
+ });
+
+ if (!plugins.length) {
+ return;
+ }
+
+ const { code } = transformSync(load(), {
+ parserOpts: {
+ plugins: ["jsx", "decorators", "typescript", "importAttributes"],
+ },
+ filename: path,
+ plugins,
+ })!;
+
+ return {
+ contents: code!,
+ loader: extname(path).slice(1) as "js" | "ts",
+ };
+ });
+ },
+});
diff --git a/src/index.test.ts b/src/index.test.ts
index 125e63c..9143ebe 100644
--- a/src/index.test.ts
+++ b/src/index.test.ts
@@ -1,14 +1,14 @@
import { promises as fs } from "node:fs";
import { resolve } from "node:path";
-import { build, transformSync } from "esbuild";
+import { type OutputFile, build, transformSync } from "esbuild";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { name } from "../package.json" with { type: "json" };
import {
type TailwindPluginOptions,
babelPlugin,
+ createPostCSS,
getClassName,
getTailwindPlugins,
- createPostCSS,
} from "./index";
const folder = resolve(import.meta.dirname, "temp");
@@ -28,8 +28,17 @@ describe("babel-tailwind", () => {
return resolved;
}
+ const minCSS = (text: string) =>
+ transformSync(text, { minify: true, loader: "css" }).code;
+
+ const findByExt = (outputFiles: OutputFile[], ext: string) =>
+ outputFiles.find(file => file.path.endsWith(ext))!;
+
async function compileESBuild(options: TailwindPluginOptions, javascript: string) {
- const tailwind = getTailwindPlugins(options);
+ const tailwind = getTailwindPlugins({
+ tailwindConfig: {},
+ ...options,
+ });
const result = await build({
bundle: true,
write: false,
@@ -37,7 +46,7 @@ describe("babel-tailwind", () => {
outdir: "dist",
format: "esm",
entryPoints: [await write("index.tsx", javascript)],
- plugins: [babelPlugin({ getPlugins: () => [tailwind.babel] }), tailwind.esbuild],
+ plugins: [babelPlugin({ plugins: [tailwind.babel] }), tailwind.esbuild],
});
const { errors, warnings, outputFiles } = result;
@@ -49,10 +58,7 @@ describe("babel-tailwind", () => {
it("supports ESBuild", async () => {
const outputFiles = await compileESBuild(
- {
- tailwindConfig: {},
- clsx: "emotion",
- },
+ { clsx: "emotion" },
/* tsx */ `
export function Hello() {
return (
@@ -65,33 +71,69 @@ describe("babel-tailwind", () => {
);
expect(outputFiles).toHaveLength(2);
- const js = outputFiles.find(file => file.path.endsWith(".js"))!;
- const css = outputFiles.find(file => file.path.endsWith(".css"))!;
+ const js = findByExt(outputFiles, ".js");
+ const css = findByExt(outputFiles, ".css");
const clsName = getClassName("text-center");
expect(js.text).toContain(`className: "${clsName}"`);
expect(css.text).toMatch(`.${clsName} {\n text-align: center;\n}`);
});
- const minCSS = (text: string) =>
- transformSync(text, { minify: true, loader: "css" })!.code;
+ it("does not remove the attribute if `preserveAttribute` is true", async () => {
+ const outputFiles = await compileESBuild(
+ { clsx: "emotion", jsxAttributeAction: "preserve" },
+ /* tsx */ `
+ export function Hello() {
+ return (
+
+ Hello, world!
+
+ );
+ }
+ `
+ );
+ expect(outputFiles).toHaveLength(2);
+
+ const js = findByExt(outputFiles, ".js");
+ expect(js.text).toContain(`css: "text-center"`);
+ });
+
+ it("supports custom jsxAttributeName", async () => {
+ const outputFiles = await compileESBuild(
+ { clsx: "emotion", jsxAttributeName: "tw" },
+ /* tsx */ `
+ export function Hello() {
+ return (
+
+ Hello, world!
+
+ );
+ }
+ `
+ );
+ expect(outputFiles).toHaveLength(2);
+
+ const js = findByExt(outputFiles, ".js");
+ const css = findByExt(outputFiles, ".css");
+
+ const clsName = getClassName("text-center");
+ expect(js.text).toContain(`className: "${clsName}"`);
+ expect(css.text).toMatch(`.${clsName} {\n text-align: center;\n}`);
+ });
it("supports importing tailwind/base", async () => {
const postcss = createPostCSS({ tailwindConfig: {} });
const base = (await postcss("@tailwind base;")).css;
const outputFiles = await compileESBuild(
- {
- tailwindConfig: {},
- clsx: "emotion",
- },
+ { clsx: "emotion" },
/* tsx */ `
import "${name}/base";
`
);
expect(outputFiles).toHaveLength(2);
- const js = outputFiles.find(file => file.path.endsWith(".js"))!;
- const css = outputFiles.find(file => file.path.endsWith(".css"))!;
+ const js = findByExt(outputFiles, ".js");
+ const css = findByExt(outputFiles, ".css");
// expect(js.text).toContain(`import "./base.css";`);
expect(js.text).toBe("");
diff --git a/src/index.ts b/src/index.ts
index 9ea2284..12bb38b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,14 +1,14 @@
-import { readFileSync } from "node:fs";
import { basename, dirname, extname, join } from "node:path";
-import { once } from "lodash";
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, transformSync } from "@babel/core";
+import { type NodePath, type types as t } from "@babel/core";
import tailwind, { type Config } from "tailwindcss";
import postcss from "postcss";
+export { babelPlugin } from "./esbuild-babel";
+
const PLUGIN_NAME = "tailwind";
const ESBUILD_NAMESPACE = "babel-tailwind";
const ROLLUP_PREFIX = "\0tailwind:";
@@ -50,7 +50,7 @@ export interface TailwindPluginOptions {
/**
* Tailwind CSS configuration
*/
- tailwindConfig: Omit;
+ tailwindConfig?: Omit;
/**
* Directives to prefix to all Tailwind stylesheets
@@ -68,6 +68,12 @@ export interface TailwindPluginOptions {
*/
jsxAttributeName?: string;
+ /**
+ * What to do with the original attribute after processing
+ * @default "delete"
+ */
+ jsxAttributeAction?: "delete" | "preserve" | ["rename", string];
+
/**
* Tagged template macro function to use for Tailwind classes
* @default "tw"
@@ -108,6 +114,7 @@ function babelTailwind(
clsx,
getClassName: getClass = getClassName,
taggedTemplateName,
+ jsxAttributeAction = "delete",
jsxAttributeName = "css",
}: TailwindPluginOptions
) {
@@ -190,8 +197,13 @@ function babelTailwind(
JSXAttribute(path, { tailwindMap, getCx }) {
const { name } = path.node;
+ if (name.name !== jsxAttributeName) return;
+
const valuePath = path.get("value");
- if (name.name !== jsxAttributeName || !valuePath.node) return;
+ 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(
@@ -246,56 +258,19 @@ function babelTailwind(
t.jsxAttribute(t.jsxIdentifier("className"), valuePath.node)
);
}
- path.remove();
+
+ if (jsxAttributeAction === "delete") {
+ path.remove();
+ } else {
+ path.node.value = copy!;
+ if (Array.isArray(jsxAttributeAction) && jsxAttributeAction[0] === "rename") {
+ path.node.name.name = jsxAttributeAction[1];
+ }
+ }
},
}));
}
-/**
- * An esbuild plugin that processes files with Babel if `getPlugins` returns any plugins.
- */
-export const babelPlugin = ({
- filter = /\.[jt]sx?$/,
- plugins: getPlugins,
-}: {
- filter?: RegExp;
- plugins:
- | babel.PluginItem[]
- | ((file: { path: string; contents: string }) => babel.PluginItem[]);
-}): esbuild.Plugin => ({
- name: "babel-plugin",
- setup(build) {
- build.onLoad({ filter }, ({ path }) => {
- const load = once(() => readFileSync(path, "utf-8"));
- const plugins = Array.isArray(getPlugins)
- ? getPlugins
- : getPlugins({
- path,
- get contents() {
- return load();
- },
- });
-
- if (!plugins.length) {
- return;
- }
-
- const { code } = transformSync(load(), {
- parserOpts: {
- plugins: ["jsx", "decorators", "typescript", "importAttributes"],
- },
- filename: path,
- plugins,
- })!;
-
- return {
- contents: code!,
- loader: extname(path).slice(1) as "js" | "ts",
- };
- });
- },
-});
-
type Compile = ReturnType;
/** @internal */