Initial commit
This commit is contained in:
commit
0ed31ebc0f
6
.eslintrc.js
Normal file
6
.eslintrc.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// @ts-check
|
||||||
|
const { extendConfig } = require("@aet/eslint-rules");
|
||||||
|
|
||||||
|
module.exports = extendConfig({
|
||||||
|
plugins: ["react"],
|
||||||
|
});
|
132
.gitignore
vendored
Normal file
132
.gitignore
vendored
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
*.draft.*
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"eslint.runtime": "node"
|
||||||
|
}
|
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) [year] [fullname]
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
43
package.json
Normal file
43
package.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "@aet/babel-tailwind",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup --dts",
|
||||||
|
"test": "vitest"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"@aet/eslint-rules": "^0.0.18",
|
||||||
|
"@types/babel__core": "^7.20.5",
|
||||||
|
"@types/lodash": "^4.17.0",
|
||||||
|
"@types/node": "^20.11.30",
|
||||||
|
"esbuild": "^0.20.2",
|
||||||
|
"esbuild-register": "^3.5.0",
|
||||||
|
"prettier": "^3.2.5",
|
||||||
|
"tailwindcss": "^3.4.3",
|
||||||
|
"tsup": "^8.0.2",
|
||||||
|
"typescript": "^5.4.3",
|
||||||
|
"vitest": "^1.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"tailwindcss": "^3.4.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/core": "^7.24.3",
|
||||||
|
"@emotion/hash": "^0.9.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"postcss": "^8.4.38"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"printWidth": 90,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
||||||
|
}
|
3591
pnpm-lock.yaml
generated
Normal file
3591
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
73
src/index.test.ts
Normal file
73
src/index.test.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { promises as fs } from "node:fs";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
import { build } from "esbuild";
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
type TailwindPluginOptions,
|
||||||
|
babelPlugin,
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
|
||||||
|
async function write(path: string, content: string) {
|
||||||
|
const resolved = resolve(folder, path);
|
||||||
|
await fs.writeFile(resolved, content);
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function compile(options: TailwindPluginOptions, javascript: string) {
|
||||||
|
const tailwind = getTailwindPlugins(options);
|
||||||
|
const result = await build({
|
||||||
|
bundle: true,
|
||||||
|
write: false,
|
||||||
|
external: ["react/jsx-runtime"],
|
||||||
|
outdir: "dist",
|
||||||
|
format: "esm",
|
||||||
|
entryPoints: [await write("index.tsx", javascript)],
|
||||||
|
plugins: [babelPlugin({ getPlugins: () => [tailwind.babel] }), tailwind.esbuild],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { errors, warnings, outputFiles } = result;
|
||||||
|
expect(errors).toHaveLength(0);
|
||||||
|
expect(warnings).toHaveLength(0);
|
||||||
|
|
||||||
|
return outputFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
it("renders", async () => {
|
||||||
|
const outputFiles = await compile(
|
||||||
|
{
|
||||||
|
tailwindConfig: {},
|
||||||
|
clsx: "emotion",
|
||||||
|
},
|
||||||
|
/* tsx */ `
|
||||||
|
export function Hello() {
|
||||||
|
return (
|
||||||
|
<div css="text-center">
|
||||||
|
Hello, world!
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
expect(outputFiles).toHaveLength(2);
|
||||||
|
|
||||||
|
const js = outputFiles.find(file => file.path.endsWith(".js"))!;
|
||||||
|
const css = outputFiles.find(file => file.path.endsWith(".css"))!;
|
||||||
|
|
||||||
|
const clsName = getClassName("text-center");
|
||||||
|
expect(js.text).toContain(`className: "${clsName}"`);
|
||||||
|
expect(css.text).toMatch(`.${clsName} {\n text-align: center;\n}`);
|
||||||
|
});
|
||||||
|
});
|
318
src/index.ts
Normal file
318
src/index.ts
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
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 NodePath, type types as t, transform } from "@babel/core";
|
||||||
|
import { type Plugin } from "esbuild";
|
||||||
|
import tailwind, { type Config } from "tailwindcss";
|
||||||
|
import postcss from "postcss";
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ESBUILD_NAMESPACE = "babel-tailwind";
|
||||||
|
|
||||||
|
export interface TailwindPluginOptions {
|
||||||
|
/**
|
||||||
|
* Tailwind CSS configuration
|
||||||
|
*/
|
||||||
|
tailwindConfig: Omit<Config, "content">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional PostCSS plugins (optional)
|
||||||
|
*/
|
||||||
|
postCSSPlugins?: postcss.AcceptedPlugin[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute to use for tailwind classes in JSX
|
||||||
|
* @default "css"
|
||||||
|
*/
|
||||||
|
jsxAttributeName?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The prefix to use for the generated class names.
|
||||||
|
* @default className => `tw-${hash(className)}`
|
||||||
|
*/
|
||||||
|
getClassName?: GetClassName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preferred library for classnames
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
|
||||||
|
const babelTailwind = (
|
||||||
|
styleMap: Map<string, string>,
|
||||||
|
{
|
||||||
|
clsx,
|
||||||
|
getClassName: getClass = getClassName,
|
||||||
|
jsxAttributeName = "css",
|
||||||
|
}: TailwindPluginOptions
|
||||||
|
) =>
|
||||||
|
definePlugin<{
|
||||||
|
getCx: () => t.Identifier;
|
||||||
|
tailwindMap: Map<string, string>;
|
||||||
|
}>(({ types: t }) => ({
|
||||||
|
Program: {
|
||||||
|
enter(path, state) {
|
||||||
|
let cx: t.Identifier;
|
||||||
|
|
||||||
|
state.tailwindMap = new Map();
|
||||||
|
|
||||||
|
function getClsxImport() {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.getCx = () => {
|
||||||
|
if (cx == null) {
|
||||||
|
cx = path.scope.generateUidIdentifier("cx");
|
||||||
|
path.node.body.unshift(getClsxImport());
|
||||||
|
}
|
||||||
|
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 }) {
|
||||||
|
const {
|
||||||
|
tag,
|
||||||
|
quasi: { quasis, expressions },
|
||||||
|
} = path.node;
|
||||||
|
if (!t.isIdentifier(tag, { name: "tw" })) return;
|
||||||
|
|
||||||
|
if (expressions.length) {
|
||||||
|
throw new Error("tw`` 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;
|
||||||
|
const valuePath = path.get("value");
|
||||||
|
if (name.name !== jsxAttributeName || !valuePath.node) return;
|
||||||
|
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
path.remove();
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An esbuild plugin that processes files with Babel if `getPlugins` returns any plugins.
|
||||||
|
*/
|
||||||
|
export const babelPlugin = ({
|
||||||
|
filter = /\.[jt]sx?$/,
|
||||||
|
getPlugins,
|
||||||
|
}: {
|
||||||
|
filter?: RegExp;
|
||||||
|
getPlugins(file: { path: string; contents: string }): babel.PluginItem[];
|
||||||
|
}): Plugin => ({
|
||||||
|
name: "babel-plugin",
|
||||||
|
setup(build) {
|
||||||
|
build.onLoad({ filter }, ({ path }) => {
|
||||||
|
const load = once(() => readFileSync(path, "utf-8"));
|
||||||
|
const plugins = getPlugins({
|
||||||
|
path,
|
||||||
|
get contents() {
|
||||||
|
return load();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!plugins.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { code } = transform(load(), {
|
||||||
|
parserOpts: {
|
||||||
|
plugins: ["jsx", "typescript"],
|
||||||
|
},
|
||||||
|
filename: path,
|
||||||
|
plugins,
|
||||||
|
})!;
|
||||||
|
|
||||||
|
return {
|
||||||
|
contents: code!,
|
||||||
|
loader: extname(path).slice(1) as "js" | "ts",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const tailwindPlugin = (
|
||||||
|
styleMap: Map<string, string>,
|
||||||
|
{ tailwindConfig, postCSSPlugins = [] }: TailwindPluginOptions
|
||||||
|
): Plugin => ({
|
||||||
|
name: "tailwind",
|
||||||
|
|
||||||
|
setup(build) {
|
||||||
|
const post = postcss([
|
||||||
|
tailwind({
|
||||||
|
...tailwindConfig,
|
||||||
|
content: [{ raw: "<br>", extension: "html" }],
|
||||||
|
}),
|
||||||
|
...postCSSPlugins,
|
||||||
|
]);
|
||||||
|
|
||||||
|
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.onLoad({ filter: /.*/, namespace: ESBUILD_NAMESPACE }, async ({ path }) => {
|
||||||
|
if (!styleMap.has(path)) return;
|
||||||
|
const result = await post.process(styleMap.get(path)!, { from: undefined });
|
||||||
|
|
||||||
|
return {
|
||||||
|
contents: result.css,
|
||||||
|
loader: "css",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry. Returns the esbuild and babel plugins for tailwind.
|
||||||
|
* @example
|
||||||
|
* import { build } from "esbuild";
|
||||||
|
* import getTailwindPlugins, { babelPlugin } from "babel-tailwind";
|
||||||
|
*
|
||||||
|
* const tailwind = getTailwindPlugins(options);
|
||||||
|
* const result = await build({
|
||||||
|
* plugins: [
|
||||||
|
* babelPlugin({ getPlugins: () => [tailwind.babel] }),
|
||||||
|
* tailwind.esbuild,
|
||||||
|
* ],
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function getTailwindPlugins(options: TailwindPluginOptions) {
|
||||||
|
const styleMap = new Map<string, string>();
|
||||||
|
return {
|
||||||
|
babel: babelTailwind(styleMap, options),
|
||||||
|
esbuild: tailwindPlugin(styleMap, options),
|
||||||
|
};
|
||||||
|
}
|
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"declaration": true,
|
||||||
|
"allowArbitraryExtensions": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"strict": true,
|
||||||
|
"stripInternal": true,
|
||||||
|
"target": "esnext",
|
||||||
|
"useUnknownInCatchVariables": false
|
||||||
|
}
|
||||||
|
}
|
15
tsup.config.ts
Normal file
15
tsup.config.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { defineConfig } from "tsup";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ["src/index.ts"],
|
||||||
|
outDir: "dist",
|
||||||
|
splitting: false,
|
||||||
|
sourcemap: false,
|
||||||
|
clean: true,
|
||||||
|
esbuildOptions(options) {
|
||||||
|
options.banner = {
|
||||||
|
...options.banner,
|
||||||
|
js: "/* eslint-disable */",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
5
vitest.config.mts
Normal file
5
vitest.config.mts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {},
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user