Add extra processing options

This commit is contained in:
Alex 2024-04-06 21:04:33 -04:00
parent 8c789367af
commit ee14d81e8e
4 changed files with 140 additions and 72 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "@aet/babel-tailwind", "name": "@aet/babel-tailwind",
"version": "0.0.1-beta.4", "version": "0.0.1-beta.5",
"main": "dist/index.js", "main": "dist/index.js",
"license": "MIT", "license": "MIT",
"private": true, "private": true,
@ -12,7 +12,7 @@
"dist" "dist"
], ],
"devDependencies": { "devDependencies": {
"@aet/eslint-rules": "^0.0.19", "@aet/eslint-rules": "^0.0.21",
"@types/babel__core": "^7.20.5", "@types/babel__core": "^7.20.5",
"@types/bun": "^1.0.12", "@types/bun": "^1.0.12",
"@types/lodash": "^4.17.0", "@types/lodash": "^4.17.0",

51
src/esbuild-babel.ts Normal file
View File

@ -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",
};
});
},
});

View File

@ -1,14 +1,14 @@
import { promises as fs } from "node:fs"; import { promises as fs } from "node:fs";
import { resolve } from "node:path"; 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 { afterEach, beforeEach, describe, expect, it } from "vitest";
import { name } from "../package.json" with { type: "json" }; import { name } from "../package.json" with { type: "json" };
import { import {
type TailwindPluginOptions, type TailwindPluginOptions,
babelPlugin, babelPlugin,
createPostCSS,
getClassName, getClassName,
getTailwindPlugins, getTailwindPlugins,
createPostCSS,
} from "./index"; } from "./index";
const folder = resolve(import.meta.dirname, "temp"); const folder = resolve(import.meta.dirname, "temp");
@ -28,8 +28,17 @@ describe("babel-tailwind", () => {
return resolved; 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) { async function compileESBuild(options: TailwindPluginOptions, javascript: string) {
const tailwind = getTailwindPlugins(options); const tailwind = getTailwindPlugins({
tailwindConfig: {},
...options,
});
const result = await build({ const result = await build({
bundle: true, bundle: true,
write: false, write: false,
@ -37,7 +46,7 @@ describe("babel-tailwind", () => {
outdir: "dist", outdir: "dist",
format: "esm", format: "esm",
entryPoints: [await write("index.tsx", javascript)], 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; const { errors, warnings, outputFiles } = result;
@ -49,10 +58,7 @@ describe("babel-tailwind", () => {
it("supports ESBuild", async () => { it("supports ESBuild", async () => {
const outputFiles = await compileESBuild( const outputFiles = await compileESBuild(
{ { clsx: "emotion" },
tailwindConfig: {},
clsx: "emotion",
},
/* tsx */ ` /* tsx */ `
export function Hello() { export function Hello() {
return ( return (
@ -65,33 +71,69 @@ describe("babel-tailwind", () => {
); );
expect(outputFiles).toHaveLength(2); expect(outputFiles).toHaveLength(2);
const js = outputFiles.find(file => file.path.endsWith(".js"))!; const js = findByExt(outputFiles, ".js");
const css = outputFiles.find(file => file.path.endsWith(".css"))!; const css = findByExt(outputFiles, ".css");
const clsName = getClassName("text-center"); const clsName = getClassName("text-center");
expect(js.text).toContain(`className: "${clsName}"`); expect(js.text).toContain(`className: "${clsName}"`);
expect(css.text).toMatch(`.${clsName} {\n text-align: center;\n}`); expect(css.text).toMatch(`.${clsName} {\n text-align: center;\n}`);
}); });
const minCSS = (text: string) => it("does not remove the attribute if `preserveAttribute` is true", async () => {
transformSync(text, { minify: true, loader: "css" })!.code; const outputFiles = await compileESBuild(
{ clsx: "emotion", jsxAttributeAction: "preserve" },
/* tsx */ `
export function Hello() {
return (
<div css="text-center">
Hello, world!
</div>
);
}
`
);
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 (
<div tw="text-center">
Hello, world!
</div>
);
}
`
);
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 () => { it("supports importing tailwind/base", async () => {
const postcss = createPostCSS({ tailwindConfig: {} }); const postcss = createPostCSS({ tailwindConfig: {} });
const base = (await postcss("@tailwind base;")).css; const base = (await postcss("@tailwind base;")).css;
const outputFiles = await compileESBuild( const outputFiles = await compileESBuild(
{ { clsx: "emotion" },
tailwindConfig: {},
clsx: "emotion",
},
/* tsx */ ` /* tsx */ `
import "${name}/base"; import "${name}/base";
` `
); );
expect(outputFiles).toHaveLength(2); expect(outputFiles).toHaveLength(2);
const js = outputFiles.find(file => file.path.endsWith(".js"))!; const js = findByExt(outputFiles, ".js");
const css = outputFiles.find(file => file.path.endsWith(".css"))!; const css = findByExt(outputFiles, ".css");
// expect(js.text).toContain(`import "./base.css";`); // expect(js.text).toContain(`import "./base.css";`);
expect(js.text).toBe(""); expect(js.text).toBe("");

View File

@ -1,14 +1,14 @@
import { readFileSync } from "node:fs";
import { basename, dirname, extname, join } from "node:path"; import { basename, dirname, extname, join } from "node:path";
import { once } from "lodash";
import hash from "@emotion/hash"; import hash from "@emotion/hash";
import type babel from "@babel/core"; import type babel from "@babel/core";
import type * as vite from "vite"; import type * as vite from "vite";
import type * as esbuild from "esbuild"; 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 tailwind, { type Config } from "tailwindcss";
import postcss from "postcss"; import postcss from "postcss";
export { babelPlugin } from "./esbuild-babel";
const PLUGIN_NAME = "tailwind"; const PLUGIN_NAME = "tailwind";
const ESBUILD_NAMESPACE = "babel-tailwind"; const ESBUILD_NAMESPACE = "babel-tailwind";
const ROLLUP_PREFIX = "\0tailwind:"; const ROLLUP_PREFIX = "\0tailwind:";
@ -50,7 +50,7 @@ export interface TailwindPluginOptions {
/** /**
* Tailwind CSS configuration * Tailwind CSS configuration
*/ */
tailwindConfig: Omit<Config, "content">; tailwindConfig?: Omit<Config, "content">;
/** /**
* Directives to prefix to all Tailwind stylesheets * Directives to prefix to all Tailwind stylesheets
@ -68,6 +68,12 @@ export interface TailwindPluginOptions {
*/ */
jsxAttributeName?: string; 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 * Tagged template macro function to use for Tailwind classes
* @default "tw" * @default "tw"
@ -108,6 +114,7 @@ function babelTailwind(
clsx, clsx,
getClassName: getClass = getClassName, getClassName: getClass = getClassName,
taggedTemplateName, taggedTemplateName,
jsxAttributeAction = "delete",
jsxAttributeName = "css", jsxAttributeName = "css",
}: TailwindPluginOptions }: TailwindPluginOptions
) { ) {
@ -190,8 +197,13 @@ function babelTailwind(
JSXAttribute(path, { tailwindMap, getCx }) { JSXAttribute(path, { tailwindMap, getCx }) {
const { name } = path.node; const { name } = path.node;
if (name.name !== jsxAttributeName) return;
const valuePath = path.get("value"); 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 parent = path.parent as t.JSXOpeningElement;
const classNameAttribute = parent.attributes.find( const classNameAttribute = parent.attributes.find(
@ -246,56 +258,19 @@ function babelTailwind(
t.jsxAttribute(t.jsxIdentifier("className"), valuePath.node) t.jsxAttribute(t.jsxIdentifier("className"), valuePath.node)
); );
} }
if (jsxAttributeAction === "delete") {
path.remove(); 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<typeof createPostCSS>; type Compile = ReturnType<typeof createPostCSS>;
/** @internal */ /** @internal */