├── .github ├── dependabot.yml └── workflows │ └── pipeline.yml ├── .gitignore ├── bun.lockb ├── index.test.ts ├── index.ts ├── license ├── package.json ├── readme.md ├── test.tailwind.css └── tsconfig.json /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "daily" 9 | 10 | - package-ecosystem: "npm" 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | -------------------------------------------------------------------------------- /.github/workflows/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: "🎭 Pipeline" 2 | 3 | on: push 4 | 5 | jobs: 6 | lint: 7 | name: "🧹 Lint" 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: oven-sh/setup-bun@v1 12 | - run: "bun install --ignore-scripts" 13 | - run: "bun lint" 14 | 15 | test: 16 | name: "🧪 Test" 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: oven-sh/setup-bun@v1 21 | - run: "bun install --ignore-scripts" 22 | - run: "bun test" 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | .devtools/3rd-party 4 | .devbox/ -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtramontina/elysia-tailwind/457249dfe0f967276413613373a00f939d96e254/bun.lockb -------------------------------------------------------------------------------- /index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "bun:test"; 2 | import Elysia from "elysia"; 3 | import { tailwind } from "./index.ts"; 4 | 5 | const get = (path: string) => 6 | new Request({ method: "GET", url: `http://localhost${path}` }); 7 | 8 | const testSettings = { 9 | source: "./test.tailwind.css", 10 | config: { 11 | content: ["**/*.ts"], 12 | theme: { extend: {} }, 13 | plugins: [], 14 | }, 15 | }; 16 | 17 | describe("elysia tailwind", () => { 18 | it("serves a compiled tailwind stylesheet on the configured path", async () => { 19 | const app = new Elysia().use( 20 | tailwind({ 21 | ...testSettings, 22 | path: "/public/stylesheet.css", 23 | }), 24 | ); 25 | 26 | const response = await app.handle(get("/public/stylesheet.css")); 27 | expect(response.status).toBe(200); 28 | expect(response.headers.get("content-type")).toBe("text/css"); 29 | expect(await response.text()).toStartWith("/*\n! tailwindcss"); 30 | }); 31 | 32 | it("minifies when NODE_ENV is production", async () => { 33 | Bun.env.NODE_ENV = "production"; 34 | 35 | const app = new Elysia().use( 36 | tailwind({ 37 | ...testSettings, 38 | path: "/public/stylesheet.css", 39 | }), 40 | ); 41 | 42 | const response = await app.handle(get("/public/stylesheet.css")); 43 | expect(await response.text()).toStartWith("/*! tailwindcss"); 44 | }); 45 | 46 | it("minifies when option is set", async () => { 47 | const app = new Elysia().use( 48 | tailwind({ 49 | ...testSettings, 50 | path: "/public/stylesheet.css", 51 | options: { minify: true }, 52 | }), 53 | ); 54 | 55 | const response = await app.handle(get("/public/stylesheet.css")); 56 | expect(await response.text()).toStartWith("/*! tailwindcss"); 57 | }); 58 | 59 | it("does not include source maps when NODE_ENV is production", async () => { 60 | Bun.env.NODE_ENV = "production"; 61 | 62 | const app = new Elysia().use( 63 | tailwind({ 64 | ...testSettings, 65 | path: "/public/stylesheet.css", 66 | }), 67 | ); 68 | 69 | const response = await app.handle(get("/public/stylesheet.css")); 70 | expect(await response.text()).not.toContain("# sourceMappingURL="); 71 | }); 72 | 73 | it("includes source maps when NODE_ENV is not production", async () => { 74 | Bun.env.NODE_ENV = "not production"; 75 | 76 | const app = new Elysia().use( 77 | tailwind({ 78 | ...testSettings, 79 | path: "/public/stylesheet.css", 80 | }), 81 | ); 82 | 83 | const response = await app.handle(get("/public/stylesheet.css")); 84 | expect(await response.text()).toContain("# sourceMappingURL="); 85 | }); 86 | 87 | it("includes source maps when option is set", async () => { 88 | const app = new Elysia().use( 89 | tailwind({ 90 | ...testSettings, 91 | path: "/public/stylesheet.css", 92 | options: { map: true }, 93 | }), 94 | ); 95 | 96 | const response = await app.handle(get("/public/stylesheet.css")); 97 | expect(await response.text()).toContain("# sourceMappingURL="); 98 | }); 99 | 100 | it("runs autoprefixer by default", async () => { 101 | const app = new Elysia().use( 102 | tailwind({ 103 | ...testSettings, 104 | path: "/public/stylesheet.css", 105 | }), 106 | ); 107 | 108 | const response = await app.handle(get("/public/stylesheet.css")); 109 | expect(await response.text()).toContain("-o-"); 110 | }); 111 | 112 | it("does not run autoprefixer when option is set to false", async () => { 113 | const app = new Elysia().use( 114 | tailwind({ 115 | ...testSettings, 116 | path: "/public/stylesheet.css", 117 | options: { autoprefixer: false }, 118 | }), 119 | ); 120 | 121 | const response = await app.handle(get("/public/stylesheet.css")); 122 | expect(await response.text()).not.toContain("-o-"); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import Elysia from "elysia"; 2 | import postcss from "postcss"; 3 | import tw, { Config } from "tailwindcss"; 4 | 5 | type Options = { 6 | minify?: boolean; 7 | map?: boolean; 8 | autoprefixer?: boolean; 9 | }; 10 | 11 | export const tailwind = (settings: { 12 | path: string; 13 | source: string; 14 | config: Config | string; 15 | options?: Options; 16 | }) => { 17 | const { 18 | path, 19 | source, 20 | config, 21 | options: { 22 | minify = Bun.env.NODE_ENV === "production", 23 | map = Bun.env.NODE_ENV !== "production", 24 | autoprefixer = true, 25 | } = {}, 26 | } = settings; 27 | 28 | const result = Bun.file(source) 29 | .text() 30 | .then((sourceText) => { 31 | const plugins = [tw(config)]; 32 | 33 | if (autoprefixer) { 34 | plugins.push(require("autoprefixer")()); 35 | } 36 | 37 | if (minify) { 38 | plugins.push(require("cssnano")()); 39 | } 40 | 41 | return postcss(...plugins).process(sourceText, { 42 | from: source, 43 | map, 44 | }); 45 | }) 46 | .then(({ css }) => css); 47 | 48 | return new Elysia({ name: "tailwind", seed: settings }).get( 49 | path, 50 | async ({ set }) => { 51 | set.headers["content-type"] = "text/css"; 52 | return result; 53 | }, 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Guilherme J. Tramontina 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gtramontina.com/elysia-tailwind", 3 | "description": "Elysia Tailwind Plugin", 4 | "version": "2.0.0", 5 | "homepage": "https://github.com/gtramontina/elysia-tailwind.git", 6 | "license": "MIT", 7 | "module": "index.ts", 8 | "files": ["index.ts"], 9 | "type": "module", 10 | "scripts": { 11 | "lint": "bun x @biomejs/biome check --vcs-client-kind=git --vcs-enabled=true --vcs-use-ignore-file=true . *.json", 12 | "lint:fix": "bun x @biomejs/biome check --apply-unsafe --vcs-client-kind=git --vcs-enabled=true --vcs-use-ignore-file=true . *.json" 13 | }, 14 | "devDependencies": { 15 | "bun-types": "1.0.26", 16 | "@types/autoprefixer": "10.2.0" 17 | }, 18 | "peerDependencies": { 19 | "autoprefixer": "^10.4.17", 20 | "cssnano": "^6.0.3", 21 | "elysia": "^0.8.16", 22 | "tailwindcss": "^3.4.1", 23 | "typescript": "^5.3.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Elysia Tailwind 2 | 3 | Elysia plugin to compile and serve Tailwind-generated stylesheets. 4 | 5 | ## Installation 6 | 7 | > [!NOTE] 8 | > This package moved to a new scope. If you were using [`elysia-tailwind`](https://www.npmjs.com/package/elysia-tailwind), you should update dependency and your imports to [`@gtramontina.com/elysia-tailwind`](https://www.npmjs.com/package/@gtramontina.com/elysia-tailwind) going forward. 9 | 10 | ```bash 11 | bun add --exact @gtramontina.com/elysia-tailwind 12 | ``` 13 | 14 | ## Usage 15 | 16 | ```ts 17 | import { tailwind } from "@gtramontina.com/elysia-tailwind"; // 1. Import 18 | import Elysia from "elysia"; 19 | 20 | new Elysia() 21 | .use(tailwind({ // 2. Use 22 | path: "/public/stylesheet.css", // 2.1 Where to serve the compiled stylesheet; 23 | source: "./source/styles.css", // 2.2 Specify source file path (where your @tailwind directives are); 24 | config: "./tailwind.config.js", // 2.3 Specify config file path or Config object; 25 | options: { // 2.4 Optionally Specify options: 26 | minify: true, // 2.4.1 Minify the output stylesheet (default: NODE_ENV === "production"); 27 | map: true, // 2.4.2 Generate source map (default: NODE_ENV !== "production"); 28 | autoprefixer: false // 2.4.3 Whether to use autoprefixer; 29 | }, 30 | })) 31 | .listen(3000); 32 | ``` 33 | -------------------------------------------------------------------------------- /test.tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext"], 4 | "module": "esnext", 5 | "target": "esnext", 6 | "moduleResolution": "bundler", 7 | "moduleDetection": "force", 8 | "allowImportingTsExtensions": true, 9 | "noEmit": true, 10 | "composite": true, 11 | "strict": true, 12 | "downlevelIteration": true, 13 | "skipLibCheck": true, 14 | "jsx": "react-jsx", 15 | "allowSyntheticDefaultImports": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "allowJs": true, 18 | "types": [ 19 | "bun-types" // add Bun global 20 | ] 21 | } 22 | } 23 | --------------------------------------------------------------------------------