babel-tailwind/src/index.test.ts
2024-06-29 11:27:31 -04:00

296 lines
7.9 KiB
TypeScript

/* eslint-disable unicorn/string-content */
import { promises as fs } from "node:fs";
import { resolve } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import * as esbuild from "esbuild";
import dedent from "dedent";
import { name } from "../package.json" with { type: "json" };
import {
type TailwindPluginOptions,
babelPlugin,
createPostCSS,
getClassName,
getTailwindPlugins,
} from "./index";
const folder = resolve(import.meta.dirname, "temp");
describe("babel-tailwind", () => {
beforeEach(async () => {
await fs.mkdir(folder, { recursive: true });
});
afterEach(async () => {
await fs.rm(folder, { recursive: true, force: true });
});
it("supports ESBuild", async () => {
const { files } = await compileESBuild({
clsx: "emotion",
expectFiles: 2,
javascript: /* tsx */ `
export function Hello() {
return (
<div css="text-center">
Hello, world!
</div>
);
}
`,
});
const clsName = getClassName("text-center");
expect(files.js.text).toContain(`className: "${clsName}"`);
expect(files.css.text).toMatch(`.${clsName} {\n text-align: center;\n}`);
});
it("does not remove the attribute if `preserveAttribute` is true", async () => {
const { files } = await compileESBuild({
clsx: "emotion",
jsxAttributeAction: "preserve",
expectFiles: 2,
javascript: /* tsx */ `
export function Hello() {
return (
<div css="text-center">
Hello, world!
</div>
);
}
`,
});
expect(files.js.text).toContain(`css: "text-center"`);
});
describe('merges with existing "className" attribute', () => {
it("string literal", async () => {
const { files } = await compileESBuild({
clsx: "emotion",
expectFiles: 2,
javascript: /* tsx */ `
export function Hello() {
return (
<div className="text-center" css="text-center">
Hello, world!
</div>
);
}
`,
});
const clsName = getClassName("text-center");
expect(files.js.text).toContain(`className: "text-center ${clsName}"`);
expect(files.css.text).toMatch(`.${clsName} {\n text-align: center;\n}`);
});
it("existing function", async () => {
const { files } = await compileESBuild({
clsx: "emotion",
expectFiles: 2,
javascript: /* tsx */ `
export function Hello() {
return (
<div className={({ isEntering }) => isEntering ? "enter" : "exit"} css="text-center">
Hello, world!
</div>
);
}
`,
});
const clsName = getClassName("text-center");
expect(files.js.text).toContain(
`className: ({\n isEntering\n }) => _cx(isEntering ? "enter" : "exit", "${clsName}")`
);
expect(files.css.text).toMatch(`.${clsName} {\n text-align: center;\n}`);
});
});
it("reports errors with correct position", async () => {
try {
await compileESBuild({
clsx: "emotion",
jsxAttributeAction: "preserve",
esbuild: {
logLevel: "silent",
},
javascript: /* tsx */ `
export function Hello() {
return (
<div css="text-center2 m-0">
Hello, world!
</div>
);
}
`,
});
throw new Error("Expected an error");
} catch (e) {
expect(e.errors).toHaveLength(1);
const [error] = e.errors;
expect(error.location).toMatchObject({
column: 14,
length: 12,
line: 3,
lineText: ' <div css="text-center2 m-0">',
});
}
});
it("supports custom jsxAttributeName", async () => {
const { files } = await compileESBuild({
clsx: "emotion",
jsxAttributeName: "tw",
expectFiles: 2,
javascript: /* tsx */ `
export function Hello() {
return (
<div tw="text-center">
Hello, world!
</div>
);
}
`,
});
const clsName = getClassName("text-center");
expect(files.js.text).toContain(`className: "${clsName}"`);
expect(files.css.text).toMatch(`.${clsName} {\n text-align: center;\n}`);
});
it("supports grouped tw", async () => {
const { files } = await compileESBuild({
clsx: "emotion",
expectFiles: 2,
javascript: /* tsx */ `
export default tw("text-sm", \`flex\`, {
"group-hover": "text-center",
"[&>div]": \`font-semibold\`,
})
`,
});
const clsName = getClassName(
"text-sm flex group-hover:text-center [&>div]:font-semibold"
);
expect(files.js.text).toContain(`= "${clsName}"`);
expect(files.css.text).toMatch(
[
`.${clsName} {`,
" display: flex;",
" font-size: 0.875rem;",
" line-height: 1.25rem;",
"}",
`.group:hover .${clsName} {`,
" text-align: center;",
"}",
`.${clsName} > div {`,
" font-weight: 600;",
"}",
].join("\n")
);
});
it("supports grouped array css jsx attribute like tw function", async () => {
const { files } = await compileESBuild({
clsx: "emotion",
expectFiles: 2,
javascript: /* tsx */ `
export function Hello() {
return (
<div css={["text-center", { hover: "font-semibold" }]}>
Hello, world!
</div>
);
}
`,
});
const clsName = ["text-center", "hover:font-semibold"].map(getClassName);
expect(files.js.text).toContain(`className: "${clsName.join(" ")}"`);
expect(files.css.text).toMatch(
[
`.${clsName[0]} {`,
" text-align: center;",
"}",
`.${clsName[1]}:hover {`,
" font-weight: 600;",
"}",
].join("\n")
);
});
it("supports importing tailwind/base", async () => {
const postcss = createPostCSS({
tailwindConfig: {},
postCSSPlugins: [],
});
const base = await postcss("@tailwind base;");
const { files } = await compileESBuild({
clsx: "emotion",
expectFiles: 2,
javascript: /* tsx */ `
import "${name}/base";
`,
});
expect(files.js.text).toBe("");
expect(minCSS(files.css.text)).toContain(minCSS(base));
});
});
async function write(path: string, content: string) {
const resolved = resolve(folder, path);
await fs.writeFile(resolved, content);
return resolved;
}
const minCSS = (text: string) =>
esbuild.transformSync(text, { minify: true, loader: "css" }).code;
const findByExt = (outputFiles: esbuild.OutputFile[], ext: string) =>
outputFiles.find(file => file.path.endsWith(ext))!;
async function compileESBuild({
javascript,
esbuild: esbuildOptions,
expectFiles,
...options
}: Omit<TailwindPluginOptions, "compile"> & {
esbuild?: esbuild.BuildOptions;
javascript: string;
expectFiles?: number;
}) {
const tailwind = getTailwindPlugins({
tailwindConfig: {},
macroFunction: "tw",
...options,
});
const result = await esbuild.build({
bundle: true,
write: false,
external: ["react/jsx-runtime", "@emotion/css", "clsx"],
outdir: "dist",
format: "esm",
entryPoints: [await write("index.tsx", dedent(javascript))],
plugins: [babelPlugin({ plugins: [tailwind.babel()] }), tailwind.esbuild()],
...esbuildOptions,
});
const { errors, warnings, outputFiles } = result;
expect(errors).toHaveLength(0);
expect(warnings).toHaveLength(0);
if (expectFiles != null) {
expect(outputFiles).toHaveLength(expectFiles);
}
return {
outputFiles: outputFiles!,
files: new Proxy({} as Record<string, esbuild.OutputFile>, {
get: (_, ext: string) => findByExt(outputFiles!, ext),
}),
};
}