From ee14d81e8ee79e80e63604b1ca0f4619309d04e5 Mon Sep 17 00:00:00 2001 From: Alex <8125011+alex-kinokon@users.noreply.github.com> Date: Sat, 6 Apr 2024 21:04:33 -0400 Subject: [PATCH] Add extra processing options --- package.json | 6 ++-- src/esbuild-babel.ts | 51 +++++++++++++++++++++++++++++ src/index.test.ts | 78 ++++++++++++++++++++++++++++++++++---------- src/index.ts | 77 +++++++++++++++---------------------------- 4 files changed, 140 insertions(+), 72 deletions(-) create mode 100644 src/esbuild-babel.ts 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 */