├── .editorconfig ├── .github └── workflows │ ├── npm-publish.yml │ └── test.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── README.md ├── build.js ├── builds ├── cdn.js └── module.ts ├── examples ├── esbuild-demo-worker │ ├── build.js │ ├── package.json │ └── src │ │ ├── index.html │ │ ├── index.ts │ │ ├── observeableWorker.ts │ │ └── tailwindcss.worker.ts ├── esbuild-demo │ ├── package.json │ ├── serve-gzip-test.js │ └── src │ │ ├── index.html │ │ └── index.ts └── script-tag │ ├── index.html │ └── package.json ├── index.d.ts ├── package-lock.json ├── package.json ├── src ├── index.ts └── stubs │ ├── crypto.ts │ ├── fs.ts │ ├── path.ts │ ├── picocolors.ts │ ├── tailwindcss │ └── utils │ │ └── log.ts │ └── url.ts ├── tests └── unit-tests │ ├── package.json │ └── src │ ├── exports.test.js │ ├── generateStyles.test.js │ ├── getClassOrder.test.js │ └── legacy.test.js ├── tsconfig.json └── types.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 100 11 | trim_trailing_whitespace = true 12 | 13 | [COMMIT_EDITMSG] 14 | max_line_length = 72 15 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Release Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | publish: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Install Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | registry-url: 'https://registry.npmjs.org/' 20 | node-version-file: '.nvmrc' 21 | cache: 'npm' 22 | 23 | - name: Set version 24 | run: npm pkg set version=${GITHUB_REF#refs/tags/} 25 | 26 | - name: Install dependencies 27 | run: npm ci 28 | 29 | - name: Build 30 | run: npm run build 31 | 32 | - name: Test 33 | run: npm run test 34 | 35 | - name: Publish to NPM 36 | run: npm publish --access public --no-git-checks 37 | env: 38 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 39 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Build and test 3 | 4 | on: 5 | workflow_dispatch: 6 | push: 7 | pull_request: 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Install Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | registry-url: 'https://registry.npmjs.org/' 19 | node-version-file: '.nvmrc' 20 | cache: 'npm' 21 | 22 | - name: Install dependencies 23 | run: npm ci 24 | 25 | - name: Build 26 | run: npm run build 27 | 28 | - name: Test 29 | run: npm run test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | .idea 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | lockfile-version = 3 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @mhsdesign/jit-browser-tailwindcss 2 | Still in Development 3 | 4 | Client side api to generate css via tailwind jit in the browser - Dynamic tailwindcss! 5 | 6 | ![image](https://user-images.githubusercontent.com/85400359/157231070-2de5d2ad-c852-40db-92dd-09d7171990bb.png) 7 | 8 | ### Tailwinds CDN: https://cdn.tailwindcss.com 9 | (Keep in mind its not only pure tailwind but also dom observing etc) 10 | with tailwind Version 3.1.8 11 | - 372kB 12 | - 98.9kB brotli 13 | - 99.4kB gzip 14 | 15 | ### This library: https://unpkg.com/@mhsdesign/jit-browser-tailwindcss 16 | (pure Tailwind API) 17 | with tailwind Version 3.1.8 18 | - 246kB 19 | - 74.3kB gzip 20 | 21 | ## Implementation 22 | 23 | ### Current implementation 24 | 25 | This library is inspired/extracted from the library [monaco-tailwindcss](https://github.com/remcohaszing/monaco-tailwindcss) 26 | see [core code link](https://github.com/remcohaszing/monaco-tailwindcss/blob/main/src/tailwindcss.worker.ts#L176) 27 | 28 | As you might see in the code [core code example](https://github.com/mhsdesign/jit-browser-tailwindcss/blob/d3b726e7122ff1d296ae50db17030a1962be36c8/src/index.ts#L17-L34) we uses tailwind internals to create a postcss plugin via `createTailwindcssPlugin`. This is not api but works for now ;) And produces highly optimized bundle sizes. 29 | 30 | ### Previous / Other implementations 31 | 32 | Previously i implemented this based on various other projects by mocking `fs.readFileSync` and having some kind of virtual file store. 33 | 34 | This turned out to be not so efficient in terms of bundle size ;) 35 | 36 |
37 | To be continued ... 38 | 39 | Also mocking `fs.readFileSync` had to be done in some postcss source files and this required the whole postcss package to be prebundled. If the developer wants to use post css too it would result postcss being in the bundle twice. 40 | 41 | See packages which are implemented like this: 42 | 43 | - [@mhsdesign/jit-browser-tailwindcss:@legacy](https://github.com/mhsdesign/jit-browser-tailwindcss/tree/legacy) 501kB (resource) - [core code link](https://github.com/mhsdesign/jit-browser-tailwindcss/blob/3604924a3d2245b64ee359edc5f19b7106943a2a/src/jitBrowserTailwindcss.js#L23-L27) 44 | 45 | - [beyondcode/tailwindcss-jit-cdn](https://github.com/beyondcode/tailwindcss-jit-cdn) 778kB (resource) - [core code link](https://github.com/beyondcode/tailwindcss-jit-cdn/blob/main/src/observer.js#L40-L52) 46 | 47 | - [tailwindlabs/play.tailwindcss.com](https://github.com/tailwindlabs/play.tailwindcss.com/) - [core code link](https://github.com/tailwindlabs/play.tailwindcss.com/blob/01c39f107a7c514b4a84ec1385926748ae5a0ef0/src/workers/processCss.js#L238-L249) 48 | 49 | The advantage here being that it uses the official API and doesnt rely much on internals. 50 | 51 |
52 | 53 | ## Supported Features of Tailwind Css 54 | everything ;) - plugins, custom Tailwind config, custom Tailwind base css, variants ... 55 | 56 | ## Usage: 57 | 58 | ### npm 59 | 60 | ```shell 61 | npm install @mhsdesign/jit-browser-tailwindcss 62 | ``` 63 | 64 | ### cdn 65 | 66 | ``` 67 | https://unpkg.com/@mhsdesign/jit-browser-tailwindcss 68 | ``` 69 | 70 | ### api to generate styles from content 71 | 72 | ```ts 73 | /** 74 | * The entry point to retrieve 'tailwindcss' 75 | * 76 | * @param options {@link TailwindcssOptions} 77 | * @example 78 | * const tailwindConfig: TailwindConfig = { 79 | * theme: { 80 | * extend: { 81 | * colors: { 82 | * marcherry: 'red', 83 | * }, 84 | * }, 85 | * }, 86 | * }; 87 | * const tailwindCss = tailwindcssFactory({ tailwindConfig }); 88 | */ 89 | export function createTailwindcss( 90 | options?: TailwindcssOptions, 91 | ): Tailwindcss; 92 | 93 | export interface TailwindcssOptions { 94 | /** 95 | * The tailwind configuration to use. 96 | */ 97 | tailwindConfig?: TailwindConfig; 98 | } 99 | 100 | export interface Tailwindcss { 101 | /** 102 | * Update the current Tailwind configuration. 103 | * 104 | * @param tailwindConfig The new Tailwind configuration. 105 | */ 106 | setTailwindConfig: (tailwindConfig: TailwindConfig) => void; 107 | 108 | /** 109 | * Generate styles using Tailwindcss. 110 | * 111 | * This generates CSS using the Tailwind JIT compiler. It uses the Tailwind configuration that has 112 | * previously been passed to {@link createTailwindcss}. 113 | * 114 | * @param css The CSS to process. Only one CSS file can be processed at a time. 115 | * @param content All content that contains CSS classes to extract. 116 | * @returns The CSS generated by the Tailwind JIT compiler. It has been optimized for the given 117 | * content. 118 | * @example 119 | * tailwindcss.generateStylesFromContent( 120 | * css, 121 | * [myHtmlCode] 122 | * ) 123 | */ 124 | generateStylesFromContent: (css: string, content: (Content | string)[]) => Promise; 125 | } 126 | 127 | /** 128 | * Contains the content of CSS classes to extract. 129 | * With optional "extension" key, which might be relevant 130 | * to properly extract css classed based on the content language. 131 | */ 132 | export interface Content { 133 | content: string; 134 | extension?: string; 135 | } 136 | ``` 137 | 138 | ### lower level api to create tailwind post css plugin 139 | 140 | ```ts 141 | /** 142 | * Lower level API to create a PostCSS Tailwindcss Plugin 143 | * @internal might change in the future 144 | * @example 145 | * const processor = postcss([createTailwindcssPlugin({ tailwindConfig, content })]); 146 | * const { css } = await processor.process(css, { from: undefined }); 147 | */ 148 | export function createTailwindcssPlugin( 149 | options: TailwindCssPluginOptions 150 | ): AcceptedPlugin; 151 | 152 | export interface TailwindCssPluginOptions { 153 | /** 154 | * The tailwind configuration to use. 155 | */ 156 | tailwindConfig?: TailwindConfig; 157 | /** 158 | * All content that contains CSS classes to extract. 159 | */ 160 | content: (Content | string)[]; 161 | } 162 | ``` 163 | 164 | ## Examples: 165 | 166 | see `examples/*` 167 | - esbuild-demo 168 | - esbuild-demo-worker 169 | - script-tag 170 | 171 | 172 | ## Use cases 173 | this plugin was developed to make dynamic html content elements from a CMS usable with tailwind classes. In that case one should already have a tailwind build and css file at hand - any further css can then be generated via this package. To have the least amount of css duplication, one should disable the normalize css and also use it without the `@base` include: 174 | 175 | ```ts 176 | const tailwind = createTailwindcss({ 177 | tailwindConfig: { 178 | // disable normalize css 179 | corePlugins: { preflight: false } 180 | } 181 | }) 182 | 183 | const css = await tailwind.generateStylesFromContent(` 184 | /* without the "@tailwind base;" */ 185 | @tailwind components; 186 | @tailwind utilities; 187 | `, [htmlContent]) 188 | ``` 189 | 190 | ## Development 191 | 192 | build the package 193 | ```sh 194 | npm run build 195 | ``` 196 | 197 | test each example (will be served with esbuild) 198 | 199 | ```sh 200 | npm run example1 201 | npm run example2 202 | npm run example3 203 | ``` 204 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | import { readdir, readFile } from 'fs/promises'; 2 | import { parse, sep } from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | import { build } from 'esbuild'; 5 | 6 | const pkg = JSON.parse(await readFile(new URL('package.json', import.meta.url))); 7 | const externalDependenciesHack = ['@tailwindcss/oxide']; 8 | 9 | /** 10 | * @type {import("esbuild").BuildOptions} 11 | */ 12 | const sharedConfig = { 13 | define: { 14 | 'process.env.OXIDE': 'undefined', 15 | 'process.env.DEBUG': 'undefined', 16 | 'process.env.JEST_WORKER_ID': '1', 17 | '__OXIDE__': 'false', 18 | __dirname: '"/"', 19 | }, 20 | plugins: [ 21 | { 22 | name: 'alias', 23 | async setup({ onLoad, onResolve, resolve }) { 24 | const stubFiles = await readdir('src/stubs', { withFileTypes: true }); 25 | // These packages are imported, but can be stubbed. 26 | const stubNames = stubFiles 27 | .filter((file) => file.isFile()) 28 | .map((file) => parse(file.name).name); 29 | onResolve({ filter: new RegExp(`^(${stubNames.join('|')})$`) }, ({ path }) => ({ 30 | path: fileURLToPath(new URL(`src/stubs/${path}.ts`, import.meta.url)), 31 | sideEffects: false, 32 | })); 33 | 34 | // The tailwindcss main export exports CJS, but we can get better tree shaking if we import 35 | // from the ESM src directoy instead. 36 | onResolve({ filter: /^tailwindcss$/ }, ({ path, ...options }) => 37 | resolve('tailwindcss/src', options), 38 | ); 39 | onResolve({ filter: /^tailwindcss\/lib/ }, ({ path, ...options }) => 40 | resolve(path.replace('lib', 'src'), options), 41 | ); 42 | 43 | // This file pulls in a number of dependencies, but we don’t really need it anyway. 44 | onResolve({ filter: /^\.+\/(util\/)?log$/, namespace: 'file' }, ({ path, ...options }) => { 45 | if (options.importer.includes(`${sep}tailwindcss${sep}`)) { 46 | return { 47 | path: fileURLToPath(new URL('src/stubs/tailwindcss/utils/log.ts', import.meta.url)), 48 | sideEffects: false, 49 | }; 50 | } 51 | return resolve(path, { 52 | ...options, 53 | sideEffects: false, 54 | namespace: 'noRecurse', 55 | }); 56 | }); 57 | 58 | // CJS doesn’t require extensions, but ESM does. Since our package uses ESM, but dependant 59 | // bundled packages don’t, we need to add it ourselves. 60 | onResolve({ filter: /^postcss-selector-parser\/.*\/\w+$/ }, ({ path, ...options }) => 61 | resolve(`${path}.js`, options), 62 | ); 63 | 64 | // minify and include the preflight.css in the javascript 65 | onLoad({ filter: /tailwindcss\/src\/css\/preflight\.css$/ }, async ({ path }) => { 66 | const result = await build({ 67 | entryPoints: [path], 68 | minify: true, 69 | logLevel: "silent", 70 | write: false 71 | }) 72 | return { contents: result.outputFiles[0].text, loader: "text" }; 73 | }); 74 | 75 | // declares all dependencies as side-effect free 76 | // currently no effect, disabled for faster build. 77 | // onResolve({ filter: /.*/, namespace: "file" }, async ({ path, ...options }) => { 78 | // const result = await resolve(path, { ...options, namespace: "noRecurse"}) 79 | // result.sideEffects = false 80 | // return result 81 | // }) 82 | 83 | // Rewrite the tailwind stubs from CJS to ESM, so our bundle doesn’t need to include any CJS 84 | // related logic. 85 | onLoad({ filter: /\/tailwindcss\/stubs\/defaultConfig\.stub\.js$/ }, async ({ path }) => { 86 | const cjs = await readFile(path, 'utf8'); 87 | const esm = cjs.replace('module.exports =', 'export default'); 88 | return { contents: esm }; 89 | }); 90 | }, 91 | }, 92 | ], 93 | } 94 | 95 | // MODULE 96 | build({ 97 | entryPoints: {'module.esm': 'builds/module.ts'}, 98 | bundle: true, 99 | external: [...Object.keys({ ...pkg.dependencies, ...pkg.peerDependencies }).filter( 100 | // We only want to include tailwindcss as an external dependency for its types. 101 | (name) => name !== 'tailwindcss', 102 | ), ...externalDependenciesHack], 103 | logLevel: 'info', 104 | outdir: 'dist', 105 | sourcemap: true, 106 | format: 'esm', 107 | ...sharedConfig 108 | }); 109 | 110 | // CDN 111 | build({ 112 | entryPoints: {'cdn.min': 'builds/cdn.js'}, 113 | external: externalDependenciesHack, 114 | bundle: true, 115 | minify: true, 116 | logLevel: 'info', 117 | format: 'iife', 118 | outdir: "dist", 119 | ...sharedConfig 120 | }); 121 | -------------------------------------------------------------------------------- /builds/cdn.js: -------------------------------------------------------------------------------- 1 | import { createTailwindcss, createTailwindcssPlugin, jitBrowserTailwindcss } from './../src/index'; 2 | 3 | window.jitBrowserTailwindcss = jitBrowserTailwindcss; 4 | window.createTailwindcss = createTailwindcss; 5 | window.createTailwindcssPlugin = createTailwindcssPlugin; 6 | -------------------------------------------------------------------------------- /builds/module.ts: -------------------------------------------------------------------------------- 1 | export { 2 | createTailwindcss, 3 | createTailwindcssPlugin, 4 | jitBrowserTailwindcss, 5 | default 6 | } from './../src/index'; 7 | -------------------------------------------------------------------------------- /examples/esbuild-demo-worker/build.js: -------------------------------------------------------------------------------- 1 | import { build, serve as esbuildServe } from 'esbuild'; 2 | 3 | const outputDir = 'dist'; 4 | 5 | /** 6 | * Todo or implement something like https://github.com/evanw/esbuild/issues/802#issuecomment-955776480 7 | * 8 | * @param {import ('esbuild').BuildOptions} opts esbuild options 9 | */ 10 | function serve(opts) { 11 | esbuildServe({ servedir: outputDir, host: '127.0.0.1'}, opts) 12 | .then((result) => { 13 | const { host, port } = result; 14 | console.log(`open: http://${host}:${port}/index.html`); 15 | }); 16 | } 17 | 18 | // Build the worker 19 | build({ 20 | entryPoints: { 21 | 'tailwindcss.worker': 'src/tailwindcss.worker.ts', 22 | }, 23 | outdir: outputDir, 24 | format: 'iife', 25 | bundle: true, 26 | minify: true, 27 | watch: true 28 | }); 29 | 30 | // Change this to `build()` for building. 31 | serve({ 32 | minify: true, 33 | entryPoints: ['src/index.ts', 'src/index.html'], 34 | bundle: true, 35 | logLevel: 'info', 36 | format: 'esm', 37 | outdir: outputDir, 38 | loader: { 39 | '.html': 'copy', 40 | }, 41 | }); 42 | -------------------------------------------------------------------------------- /examples/esbuild-demo-worker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esbuild-demo-worker", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "start": "rm -rf ./dist && node build.js" 8 | }, 9 | "dependencies": { 10 | "esbuild": "^0.15.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/esbuild-demo-worker/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Demo Dynamic Tailwind 6 | 7 | 8 | 9 | 10 |
11 |
14 |

15 | Demo Dynamic Tailwind 16 | Render Tailwind in the Browser 17 |

18 |
19 |
20 | Github 25 |
26 |
27 | Me on Github 32 |
33 |
34 |
35 |
36 | 37 |

38 | I have to Tailwind class generated. 39 | But me 40 |

41 |

Normalize.css is not Included.

42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/esbuild-demo-worker/src/index.ts: -------------------------------------------------------------------------------- 1 | import { TailwindConfig, Tailwindcss } from 'jit-browser-tailwindcss'; 2 | import { createMessenger } from './observeableWorker'; 3 | 4 | const worker = new Worker(new URL('tailwindcss.worker.js', import.meta.url).pathname); 5 | 6 | const postMessage = createMessenger(worker) 7 | 8 | const tailwindcss: Tailwindcss = { 9 | async setTailwindConfig(tailwindConfig) { 10 | await postMessage("setTailwindConfig", { tailwindConfig }) 11 | }, 12 | async generateStylesFromContent(css, content) { 13 | return postMessage("generateStylesFromContent", { css, content }) 14 | } 15 | } 16 | 17 | const tailwindConfig: TailwindConfig = { 18 | theme: { 19 | extend: { 20 | colors: { 21 | marcherry: 'red', 22 | }, 23 | }, 24 | }, 25 | }; 26 | 27 | tailwindcss.setTailwindConfig(tailwindConfig); 28 | 29 | const contentElements = document.querySelectorAll('[data-dynamic-tailwind-css]'); 30 | 31 | const content = Array.from(contentElements).reduce((carry, el) => carry + el.outerHTML, ''); 32 | 33 | const css = await tailwindcss.generateStylesFromContent( 34 | ` 35 | @tailwind base; 36 | @tailwind components; 37 | @tailwind utilities; 38 | `, 39 | [content], 40 | ); 41 | 42 | const style = document.getElementById('tailwind')!; 43 | style.textContent = css; 44 | 45 | await new Promise((r) => setTimeout(r, 1000)); 46 | 47 | tailwindcss.setTailwindConfig({ 48 | theme: { 49 | extend: { 50 | colors: { 51 | marcherry: 'blue', 52 | }, 53 | }, 54 | }, 55 | }); 56 | 57 | style.textContent = await tailwindcss.generateStylesFromContent( 58 | ` 59 | @tailwind base; 60 | @tailwind components; 61 | @tailwind utilities; 62 | `, 63 | [content], 64 | ); 65 | -------------------------------------------------------------------------------- /examples/esbuild-demo-worker/src/observeableWorker.ts: -------------------------------------------------------------------------------- 1 | 2 | let messageCount = 0; 3 | 4 | export const createMessenger = (worker: Worker) => { 5 | return (type: string, payload: any): Promise => { 6 | const id = messageCount++; 7 | const responseFromWorker = new Promise((resolve) => { 8 | const listener = (e: MessageEvent) => { 9 | if (e.data.id !== id) { 10 | return; 11 | } 12 | worker.removeEventListener("message", listener) 13 | resolve(e.data.payload) 14 | } 15 | worker.addEventListener("message", listener) 16 | }) 17 | worker.postMessage({ 18 | id, 19 | type, 20 | payload 21 | }); 22 | return responseFromWorker; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/esbuild-demo-worker/src/tailwindcss.worker.ts: -------------------------------------------------------------------------------- 1 | import { createTailwindcss } from 'jit-browser-tailwindcss'; 2 | 3 | let tailwindcss = createTailwindcss(); 4 | 5 | self.onmessage = async (e) => { 6 | const { id, type, payload } = e.data; 7 | 8 | const postMessage = (payload?: any) => { 9 | self.postMessage({ 10 | id, 11 | payload 12 | }); 13 | } 14 | 15 | switch (type) { 16 | case "setTailwindConfig": 17 | tailwindcss.setTailwindConfig(payload.tailwindConfig) 18 | postMessage() 19 | break; 20 | 21 | case "generateStylesFromContent": 22 | const css = await tailwindcss.generateStylesFromContent(payload.css, payload.content) 23 | postMessage(css) 24 | break; 25 | 26 | default: 27 | throw new TypeError(`Worker: Invalid type ${type}`); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/esbuild-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esbuild-demo", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "start": "esbuild src/index.ts src/index.html --minify --bundle --format=iife --outdir=dist --loader:.html=copy --servedir=dist" 8 | }, 9 | "dependencies": { 10 | "@tailwindcss/typography": "^0.5.9", 11 | "esbuild": "^0.15.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/esbuild-demo/serve-gzip-test.js: -------------------------------------------------------------------------------- 1 | 2 | import { build, serve } from "esbuild"; 3 | import { createServer, request } from "http"; 4 | import { spawn } from "child_process"; 5 | 6 | import process from "process"; 7 | 8 | const outputDir = 'dist'; 9 | 10 | /** 11 | * @param {http.ServerRequest} req 12 | * @param {http.ServerResponse} res 13 | * @return {boolean} Whether gzip encoding takes place 14 | */ 15 | function gzip(req, res) { 16 | // check if the client accepts gzip 17 | var header = req.headers['accept-encoding']; 18 | var accepts = Boolean(header && /gzip/i.test(header)); 19 | if (!accepts) return false; 20 | 21 | // store native methods 22 | var writeHead = res.writeHead; 23 | var write = res.write; 24 | var end = res.end; 25 | 26 | var gzip = spawn('gzip'); 27 | gzip.stdout.on('data', function (chunk) { 28 | write.call(res, chunk); 29 | }); 30 | gzip.on('exit', function () { 31 | end.call(res); 32 | }); 33 | 34 | // duck punch gzip piping 35 | res.writeHead = function (status, headers) { 36 | headers = headers || {}; 37 | 38 | if (Array.isArray(headers)) { 39 | headers.push([ 'content-encoding', 'gzip' ]); 40 | } else { 41 | headers['content-encoding'] = 'gzip'; 42 | } 43 | 44 | writeHead.call(res, status, headers); 45 | }; 46 | res.write = function (chunk) { 47 | gzip.stdin.write(chunk); 48 | }; 49 | res.end = function () { 50 | gzip.stdin.end(); 51 | }; 52 | 53 | return true; 54 | }; 55 | 56 | 57 | build({ 58 | minify: true, 59 | entryPoints: ['src/index.ts', 'src/index.html'], 60 | bundle: true, 61 | logLevel: 'info', 62 | format: 'esm', 63 | outdir: outputDir, 64 | loader: { 65 | '.html': 'copy', 66 | }, 67 | watch: { 68 | onRebuild(error, result) { 69 | clients.forEach((res) => res.write("data: update\n\n")); 70 | clients.length = 0; 71 | console.log(error ? error : "..."); 72 | }, 73 | }, 74 | }) 75 | .catch(() => process.exit(1)); 76 | 77 | serve({ servedir: "./dist" }, {}).then(() => { 78 | createServer((req, res) => { 79 | 80 | const { url, method, headers } = req; 81 | 82 | // 86.8 kB 83 | 84 | // if(url === "/tailwindcss.worker.js") { 85 | 86 | // readFile('./tailwindcss.worker.js.gz', function(err, data) { 87 | // res.writeHead(200, { 88 | // ...res.headers, 89 | // 'content-encoding': 'gzip', 90 | // }); 91 | // res.write(data); 92 | 93 | // res.end() 94 | // }); 95 | 96 | // return; 97 | // } 98 | 99 | 100 | 101 | if (req.url === "/esbuild") 102 | return clients.push( 103 | res.writeHead(200, { 104 | "Content-Type": "text/event-stream", 105 | "Cache-Control": "no-cache", 106 | Connection: "keep-alive", 107 | }) 108 | ); 109 | const path = ~url.split("/").pop().indexOf(".") ? url : `/index.html`; //for PWA with router 110 | 111 | req.pipe( 112 | request( 113 | { hostname: "0.0.0.0", port: 8000, path, method, headers }, 114 | (prxRes) => { 115 | if (url === "/index.js") { 116 | 117 | const jsReloadCode = 118 | ' (() => new EventSource("/esbuild").onmessage = () => location.reload())();'; 119 | 120 | const newHeaders = { 121 | ...prxRes.headers, 122 | "content-length": 123 | parseInt(prxRes.headers["content-length"], 10) + 124 | jsReloadCode.length, 125 | }; 126 | 127 | gzip(req, res); 128 | 129 | 130 | res.writeHead(prxRes.statusCode, newHeaders); 131 | res.write(jsReloadCode); 132 | 133 | 134 | } else { 135 | 136 | // else if(url === "/tailwindcss.worker.js.gz") { 137 | 138 | // const newHeaders = { 139 | // ...prxRes.headers, 140 | // "content-type": "text/javascript", 141 | // 'content-encoding': 'gzip' 142 | // }; 143 | 144 | // res.writeHead(200, newHeaders); 145 | 146 | 147 | // } 148 | 149 | 150 | res.writeHead(prxRes.statusCode, prxRes.headers); 151 | 152 | } 153 | prxRes.pipe(res, { end: true }); 154 | } 155 | ), 156 | { end: true } 157 | ); 158 | }).listen(3000); 159 | 160 | console.log(`Open at http://localhost:3000`); 161 | }); 162 | -------------------------------------------------------------------------------- /examples/esbuild-demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Demo Dynamic Tailwind 6 | 7 | 8 | 9 | 10 |
11 |
14 |

15 | Demo Dynamic Tailwind 16 | Render Tailwind in the Browser 17 |

18 |
19 |
20 | Github 25 |
26 |
27 | Me on Github 32 |
33 |
34 |
35 |
36 | 37 |

38 | I have to Tailwind class generated. 39 | But me 40 |

41 |

Normalize.css is not Included.

42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/esbuild-demo/src/index.ts: -------------------------------------------------------------------------------- 1 | import { TailwindConfig, createTailwindcss } from 'jit-browser-tailwindcss'; 2 | // import typography from '@tailwindcss/typography'; 3 | 4 | async function init() { 5 | 6 | const tailwindConfig: TailwindConfig = { 7 | theme: { 8 | extend: { 9 | colors: { 10 | marcherry: 'red', 11 | }, 12 | }, 13 | }, 14 | // plugins: [typography] 15 | }; 16 | 17 | const tailwindCss = createTailwindcss({ tailwindConfig }); 18 | 19 | const contentElements = document.querySelectorAll('[data-dynamic-tailwind-css]'); 20 | 21 | const content = Array.from(contentElements).reduce((carry, el) => carry + el.outerHTML, ''); 22 | 23 | const css = await tailwindCss.generateStylesFromContent( 24 | ` 25 | @tailwind base; 26 | @tailwind components; 27 | @tailwind utilities; 28 | `, 29 | [content], 30 | ); 31 | 32 | const style = document.getElementById('tailwind')!; 33 | style.textContent = css; 34 | 35 | await new Promise((r) => setTimeout(r, 1000)); 36 | 37 | tailwindCss.setTailwindConfig({ 38 | theme: { 39 | extend: { 40 | colors: { 41 | marcherry: 'blue', 42 | }, 43 | }, 44 | }, 45 | }); 46 | 47 | style.textContent = await tailwindCss.generateStylesFromContent( 48 | ` 49 | @tailwind base; 50 | @tailwind components; 51 | @tailwind utilities; 52 | `, 53 | [content], 54 | ); 55 | 56 | } 57 | 58 | init() 59 | -------------------------------------------------------------------------------- /examples/script-tag/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Demo Dynamic Tailwind 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |

14 | Demo Dynamic Tailwind 15 | Render Tailwind in the Browser 16 |

17 |
18 |
19 | Github 20 |
21 |
22 | Me on Github 23 |
24 |
25 |
26 |
27 | 28 |

I have to Tailwind class generated. But me

29 |

Normalize.css is not Included.

30 | 31 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /examples/script-tag/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "script-tag", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "start": "esbuild index.html ../../dist/cdn.min.js --loader:.html=copy --loader:.js=copy --outdir=dist --entry-names=[name] --servedir=dist" 8 | }, 9 | "dependencies": { 10 | "esbuild": "^0.15.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { AcceptedPlugin } from 'postcss'; 2 | import { Config } from 'tailwindcss'; 3 | 4 | /** 5 | * The entry point to retrieve 'tailwindcss' 6 | * 7 | * @param options {@link TailwindcssOptions} 8 | * @example 9 | * const tailwindConfig: TailwindConfig = { 10 | * theme: { 11 | * extend: { 12 | * colors: { 13 | * marcherry: 'red', 14 | * }, 15 | * }, 16 | * }, 17 | * }; 18 | * const tailwindCss = tailwindcssFactory({ tailwindConfig }); 19 | */ 20 | export function createTailwindcss( 21 | options?: TailwindcssOptions, 22 | ): Tailwindcss; 23 | 24 | export interface TailwindcssOptions { 25 | /** 26 | * The tailwind configuration to use. 27 | */ 28 | tailwindConfig?: TailwindConfig; 29 | } 30 | 31 | export interface Tailwindcss { 32 | /** 33 | * Update the current Tailwind configuration. 34 | * 35 | * @param tailwindConfig The new Tailwind configuration. 36 | */ 37 | setTailwindConfig: (tailwindConfig: TailwindConfig) => void; 38 | 39 | /** 40 | * Generate styles using Tailwindcss. 41 | * 42 | * This generates CSS using the Tailwind JIT compiler. It uses the Tailwind configuration that has 43 | * previously been passed to {@link createTailwindcss}. 44 | * 45 | * @param css The CSS to process. Only one CSS file can be processed at a time. 46 | * @param content All content that contains CSS classes to extract. 47 | * @returns The CSS generated by the Tailwind JIT compiler. It has been optimized for the given 48 | * content. 49 | * @example 50 | * tailwindcss.generateStylesFromContent( 51 | * css, 52 | * [myHtmlCode] 53 | * ) 54 | */ 55 | generateStylesFromContent: (css: string, content: (Content | string)[]) => Promise 56 | 57 | /** 58 | * Get the class order for the provided list of classes 59 | * 60 | * @param classList The list of classes to get the order for. 61 | * @returns The ordered list of classes. 62 | * @example 63 | * tailwindcss.getClassOrder(['left-3', 'inset-x-2', bg-red-500', 'bg-blue-500']) 64 | */ 65 | getClassOrder: (classList: string[]) => string[] 66 | } 67 | 68 | /** 69 | * Lower level API to create a PostCSS Tailwindcss Plugin 70 | * @internal might change in the future 71 | * @example 72 | * const processor = postcss([createTailwindcssPlugin({ tailwindConfig, content })]); 73 | * const { css } = await processor.process(css, { from: undefined }); 74 | */ 75 | export function createTailwindcssPlugin( 76 | options: TailwindCssPluginOptions 77 | ): AcceptedPlugin; 78 | 79 | export interface TailwindCssPluginOptions { 80 | /** 81 | * The tailwind configuration to use. 82 | */ 83 | tailwindConfig?: TailwindConfig; 84 | /** 85 | * All content that contains CSS classes to extract. 86 | */ 87 | content: (Content | string)[]; 88 | } 89 | 90 | /** 91 | * Contains the content of CSS classes to extract. 92 | * With optional "extension" key, which might be relevant 93 | * to properly extract css classed based on the content language. 94 | */ 95 | export interface Content { 96 | content: string; 97 | extension?: string; 98 | } 99 | 100 | /** 101 | * Client side api to generate css via tailwind jit in the browser 102 | * 103 | * @deprecated with 0.2.0 104 | */ 105 | declare function jitBrowserTailwindcss(tailwindMainCss: string, jitContent: string, userTailwindConfig?: TailwindConfig): Promise; 106 | 107 | export { jitBrowserTailwindcss }; 108 | 109 | export default jitBrowserTailwindcss; 110 | 111 | // This way we Omit `content`, somehow, Omit<> doesnt work. 112 | export interface TailwindConfig { 113 | important?: Config['important']; 114 | prefix?: Config['prefix']; 115 | separator?: Config['separator']; 116 | safelist?: Config['safelist']; 117 | presets?: Config['presets']; 118 | future?: Config['future']; 119 | experimental?: Config['experimental']; 120 | darkMode?: Config['darkMode']; 121 | theme?: Config['theme']; 122 | corePlugins?: Config['corePlugins']; 123 | plugins?: Config['plugins']; 124 | } 125 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mhsdesign/jit-browser-tailwindcss", 3 | "description": "Jit Browser Tailwindcss", 4 | "workspaces": [ 5 | "examples/*", 6 | "tests/*" 7 | ], 8 | "module": "dist/module.esm.js", 9 | "main": "dist/module.esm.js", 10 | "unpkg": "dist/cdn.min.js", 11 | "files": [ 12 | "index.d.ts", 13 | "dist/*" 14 | ], 15 | "type": "module", 16 | "scripts": { 17 | "build": "node build.js", 18 | "example1": "npm --workspace esbuild-demo start", 19 | "example2": "npm --workspace esbuild-demo-worker start", 20 | "example3": "npm --workspace script-tag start", 21 | "test": "npm --workspace unit-tests run test" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/mhsdesign/jit-browser-tailwindcss.git" 26 | }, 27 | "keywords": [ 28 | "tailwind", 29 | "tailwindcss" 30 | ], 31 | "author": "Marc Henry Schultz", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/mhsdesign/jit-browser-tailwindcss/issues" 35 | }, 36 | "dependencies": { 37 | "color-name": "^1.0.0", 38 | "didyoumean": "^1.0.0", 39 | "dlv": "^1.0.0", 40 | "postcss": "^8.0.0", 41 | "postcss-js": "^4.0.0", 42 | "postcss-nested": "^5.0.0", 43 | "postcss-selector-parser": "^6.0.0", 44 | "postcss-value-parser": "^4.0.0", 45 | "quick-lru": "^5.0.0", 46 | "tailwindcss": "~3.4.0" 47 | }, 48 | "devDependencies": { 49 | "esbuild": "^0.15.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss'; 2 | import processTailwindFeatures from 'tailwindcss/src/processTailwindFeatures.js'; 3 | // @ts-ignore 4 | import { createContext } from 'tailwindcss/src/lib/setupContextUtils.js' 5 | import resolveConfig from 'tailwindcss/src/public/resolve-config.js'; 6 | 7 | export function bigSign(bigIntValue: bigint) { 8 | return Number(bigIntValue > 0n) - Number(bigIntValue < 0n) 9 | } 10 | 11 | function defaultSort(arrayOfTuples: [string, bigint | null][]) { 12 | return arrayOfTuples 13 | .sort(([, a], [, z]) => { 14 | if (a === z) return 0 15 | if (a === null) return -1 16 | if (z === null) return 1 17 | return bigSign(a - z) 18 | }) 19 | .map(([className]) => className) 20 | } 21 | 22 | export const createTailwindcss: typeof import('..').createTailwindcss = ( 23 | { tailwindConfig } = {}, 24 | ) => { 25 | 26 | let currentTailwindConfig = tailwindConfig; 27 | 28 | return { 29 | setTailwindConfig(newTailwindConfig) { 30 | currentTailwindConfig = newTailwindConfig; 31 | }, 32 | 33 | async generateStylesFromContent(css, content) { 34 | const tailwindcssPlugin = createTailwindcssPlugin({ tailwindConfig: currentTailwindConfig, content }); 35 | const processor = postcss([tailwindcssPlugin]); 36 | const result = await processor.process(css, { from: undefined }); 37 | return result.css; 38 | }, 39 | 40 | getClassOrder: (classList: string[]) => { 41 | const context = createContext(resolveConfig(tailwindConfig ?? {})) 42 | return defaultSort(context.getClassOrder(classList)) 43 | }, 44 | } 45 | } 46 | 47 | export const createTailwindcssPlugin: typeof import('..').createTailwindcssPlugin = ({ tailwindConfig, content: contentCollection }) => { 48 | const config = resolveConfig(tailwindConfig ?? {}); 49 | const tailwindcssPlugin = processTailwindFeatures( 50 | (processOptions) => () => processOptions.createContext( 51 | config, 52 | contentCollection.map((content) => (typeof content === 'string' ? { content } : content)) 53 | ), 54 | ); 55 | return tailwindcssPlugin; 56 | } 57 | 58 | export const jitBrowserTailwindcss: typeof import('..').default = (tailwindMainCss, jitContent, userTailwindConfig = {}) => { 59 | const tailwindcss = createTailwindcss({tailwindConfig: userTailwindConfig}) 60 | return tailwindcss.generateStylesFromContent(tailwindMainCss, [jitContent]) 61 | } 62 | 63 | export default jitBrowserTailwindcss; 64 | 65 | -------------------------------------------------------------------------------- /src/stubs/crypto.ts: -------------------------------------------------------------------------------- 1 | export default null; 2 | -------------------------------------------------------------------------------- /src/stubs/fs.ts: -------------------------------------------------------------------------------- 1 | import preflight from 'tailwindcss/src/css/preflight.css'; 2 | 3 | export default { 4 | // Reading the preflight CSS is the only use of fs at the moment of writing. 5 | readFileSync: () => preflight, 6 | }; 7 | -------------------------------------------------------------------------------- /src/stubs/path.ts: -------------------------------------------------------------------------------- 1 | export const join = (): string => ''; 2 | -------------------------------------------------------------------------------- /src/stubs/picocolors.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | yellow: (input: string) => input, 3 | }; 4 | -------------------------------------------------------------------------------- /src/stubs/tailwindcss/utils/log.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-empty-function 2 | export function log(): void {} 3 | 4 | export function dim(input: string): string { 5 | return input; 6 | } 7 | 8 | export default { 9 | info: log, 10 | warn: log, 11 | risk: log, 12 | }; 13 | -------------------------------------------------------------------------------- /src/stubs/url.ts: -------------------------------------------------------------------------------- 1 | export default null; 2 | -------------------------------------------------------------------------------- /tests/unit-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unit-tests", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" npx jest" 8 | }, 9 | "dependencies": { 10 | "@mhsdesign/jit-browser-tailwindcss": "file:../..", 11 | "jest": "^29.0.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/unit-tests/src/exports.test.js: -------------------------------------------------------------------------------- 1 | // FIXME i rather want to write "@mhsdesign/jit-browser-tailwindcss" here 2 | import {createTailwindcss, createTailwindcssPlugin} from "../../../dist/module.esm"; 3 | 4 | test('exports', () => { 5 | expect(typeof createTailwindcss).toBe("function"); 6 | expect(typeof createTailwindcssPlugin).toBe("function"); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/unit-tests/src/generateStyles.test.js: -------------------------------------------------------------------------------- 1 | // import {createTailwindcss} from "@mhsdesign/jit-browser-tailwindcss"; 2 | import {createTailwindcss} from "../../../dist/module.esm"; 3 | 4 | test('simple style', async () => { 5 | 6 | const tailwind = createTailwindcss({ 7 | tailwindConfig: { 8 | // disable normalize css 9 | corePlugins: { preflight: false } 10 | } 11 | }) 12 | 13 | const htmlContent = ` 14 |
15 | `; 16 | 17 | /* without the "@tailwind base;" */ 18 | const css = await tailwind.generateStylesFromContent(`@tailwind components; @tailwind utilities;`, [htmlContent]) 19 | 20 | expect(css).toBe(`.bg-red-100 { 21 | --tw-bg-opacity: 1; 22 | background-color: rgb(254 226 226 / var(--tw-bg-opacity)) 23 | }`); 24 | }); 25 | 26 | test('tailwind base', async () => { 27 | const tailwind = createTailwindcss({ 28 | tailwindConfig: { 29 | // disable normalize css 30 | corePlugins: { preflight: false } 31 | } 32 | }) 33 | 34 | const css = await tailwind.generateStylesFromContent(`@tailwind base;`, ['']) 35 | 36 | expect(css).toContain('*, ::before, ::after {'); 37 | }); 38 | 39 | 40 | test('jit custom color', async () => { 41 | 42 | const tailwind = createTailwindcss({ 43 | tailwindConfig: { 44 | // disable normalize css 45 | corePlugins: { preflight: false } 46 | } 47 | }) 48 | 49 | const htmlContent = ` 50 |
51 | `; 52 | 53 | /* without the "@tailwind base;" */ 54 | const css = await tailwind.generateStylesFromContent(`@tailwind components; @tailwind utilities;`, [htmlContent]) 55 | 56 | expect(css).toBe(`.bg-\\[\\#3f3524\\] { 57 | --tw-bg-opacity: 1; 58 | background-color: rgb(63 53 36 / var(--tw-bg-opacity)) 59 | }`); 60 | }); 61 | 62 | test('jit chained modifiers', async () => { 63 | 64 | const tailwind = createTailwindcss({ 65 | tailwindConfig: { 66 | // disable normalize css 67 | corePlugins: { preflight: false } 68 | } 69 | }) 70 | 71 | const htmlContent = ` 72 |
73 | `; 74 | 75 | /* without the "@tailwind base;" */ 76 | const css = await tailwind.generateStylesFromContent(`@tailwind components; @tailwind utilities;`, [htmlContent]) 77 | 78 | expect(css).toBe(`@media (min-width: 768px) { 79 | .focus\\:hover\\:md\\:w-full:hover:focus { 80 | width: 100% 81 | } 82 | }`); 83 | }) 84 | 85 | test('custom config', async () => { 86 | 87 | const tailwind = createTailwindcss({ 88 | tailwindConfig: { 89 | // disable normalize css 90 | corePlugins: { preflight: false }, 91 | theme: { 92 | extend: { 93 | colors: { 94 | marcherry: 'red', 95 | }, 96 | }, 97 | }, 98 | }, 99 | }) 100 | 101 | const htmlContent = ` 102 |
103 | `; 104 | 105 | /* without the "@tailwind base;" */ 106 | const css = await tailwind.generateStylesFromContent(`@tailwind components; @tailwind utilities;`, [htmlContent]) 107 | 108 | expect(css).toBe(`.bg-marcherry { 109 | --tw-bg-opacity: 1; 110 | background-color: rgb(255 0 0 / var(--tw-bg-opacity)) 111 | }`); 112 | }); 113 | 114 | test('media queries', async () => { 115 | 116 | const tailwind = createTailwindcss({ 117 | tailwindConfig: { 118 | // disable normalize css 119 | corePlugins: { preflight: false }, 120 | }, 121 | }) 122 | 123 | const htmlContent = ` 124 |
125 | `; 126 | 127 | /* without the "@tailwind base;" */ 128 | const css = await tailwind.generateStylesFromContent(`@tailwind components; @tailwind utilities;`, [htmlContent]) 129 | 130 | expect(css).toBe(`@media (min-width: 1024px) { 131 | .lg\\:py-12 { 132 | padding-top: 3rem; 133 | padding-bottom: 3rem 134 | } 135 | }`); 136 | }); 137 | -------------------------------------------------------------------------------- /tests/unit-tests/src/getClassOrder.test.js: -------------------------------------------------------------------------------- 1 | import { createTailwindcss } from '../../../dist/module.esm' 2 | 3 | test('getClassOrder', async () => { 4 | const tailwind = createTailwindcss({ 5 | tailwindConfig: { 6 | corePlugins: { preflight: false }, 7 | }, 8 | }) 9 | 10 | const cases = [ 11 | { 12 | input: 'px-3 b-class p-1 py-3 bg-blue-500 a-class bg-red-500', 13 | output: 'b-class a-class bg-blue-500 bg-red-500 p-1 px-3 py-3', 14 | }, 15 | { 16 | input: 'a-class px-3 p-1 b-class py-3 bg-red-500 bg-blue-500', 17 | output: 'a-class b-class bg-blue-500 bg-red-500 p-1 px-3 py-3', 18 | }, 19 | { 20 | input: 'left-5 left-1', 21 | output: 'left-1 left-5', 22 | }, 23 | { 24 | input: 'left-3 inset-x-10', 25 | output: 'inset-x-10 left-3', 26 | }, 27 | { 28 | input: 'left-3 inset-x-2 bg-red-500 bg-blue-500', 29 | output: 'inset-x-2 left-3 bg-blue-500 bg-red-500', 30 | }, 31 | ] 32 | 33 | for (const { input, output } of cases) { 34 | expect(tailwind.getClassOrder(input.split(' '))).toEqual(output.split(' ')) 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /tests/unit-tests/src/legacy.test.js: -------------------------------------------------------------------------------- 1 | import {jitBrowserTailwindcss} from "../../../dist/module.esm"; 2 | 3 | test('legacy api is exported', () => { 4 | expect(typeof jitBrowserTailwindcss).toBe("function"); 5 | }); 6 | 7 | test('legacy api works', async () => { 8 | const css = await jitBrowserTailwindcss(`@tailwind components; @tailwind utilities;`, 'bg-red-100'); 9 | expect(css).toContain(`.bg-red-100`) 10 | }); 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "lib": ["dom", "esnext", "dom.iterable"], 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "noEmit": true, 8 | "strict": true, 9 | "stripInternal": true, 10 | "target": "esnext" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' { 2 | const css: string; 3 | export default css; 4 | } 5 | 6 | declare module 'tailwindcss/tailwindconfig.faketype' { 7 | import { Config } from 'tailwindcss'; 8 | 9 | // This way we Omit `content`, somehow, Omit<> doesnt work. 10 | export interface TailwindConfig { 11 | important?: Config['important']; 12 | prefix?: Config['prefix']; 13 | separator?: Config['separator']; 14 | safelist?: Config['safelist']; 15 | presets?: Config['presets']; 16 | future?: Config['future']; 17 | experimental?: Config['experimental']; 18 | darkMode?: Config['darkMode']; 19 | theme?: Config['theme']; 20 | corePlugins?: Config['corePlugins']; 21 | plugins?: Config['plugins']; 22 | } 23 | } 24 | 25 | declare module 'tailwindcss/src/lib/expandApplyAtRules.js' { 26 | export default function expandApplyAtRules(): void; 27 | } 28 | 29 | declare module 'tailwindcss/src/lib/generateRules.js' { 30 | export function generateRules(): void; 31 | } 32 | 33 | declare module 'tailwindcss/src/lib/setupContextUtils.js' { 34 | import { Container } from 'postcss'; 35 | import { TailwindConfig } from 'tailwindcss/tailwindconfig.faketype'; 36 | 37 | interface ChangedContent { 38 | content: string; 39 | extension?: string; 40 | } 41 | 42 | interface Api { 43 | container: Container; 44 | separator: string; 45 | format: (def: string) => void; 46 | wrap: (rule: Container) => void; 47 | } 48 | 49 | type VariantPreview = string; 50 | 51 | type VariantFn = [number, (api: Api) => VariantPreview | undefined]; 52 | 53 | type VariantName = string; 54 | 55 | export interface JitContext { 56 | changedContent: ChangedContent[]; 57 | getClassList: () => string[]; 58 | getClassOrder: (classList: string[]) => Array<[string, bigint | null]>; 59 | tailwindConfig: TailwindConfig; 60 | variantMap: Map; 61 | } 62 | 63 | export function createContext( 64 | config: TailwindConfig, 65 | changedContent?: ChangedContent[], 66 | ): JitContext; 67 | } 68 | 69 | declare module 'tailwindcss/src/processTailwindFeatures.js' { 70 | import { AtRule, Plugin, Result, Root } from 'postcss'; 71 | import { ChangedContent, JitContext } from 'tailwindcss/src/lib/setupContextUtils.js'; 72 | import { TailwindConfig } from 'tailwindcss/tailwindconfig.faketype'; 73 | 74 | type SetupContext = (root: Root, result: Result) => JitContext; 75 | 76 | interface ProcessTailwindFeaturesCallbackOptions { 77 | applyDirectives: Set; 78 | createContext: (config: TailwindConfig, changedContent: ChangedContent[]) => JitContext; 79 | registerDependency: () => unknown; 80 | tailwindDirectives: Set; 81 | } 82 | 83 | export default function processTailwindFeatures( 84 | callback: (options: ProcessTailwindFeaturesCallbackOptions) => SetupContext, 85 | ): Plugin; 86 | } 87 | 88 | declare module 'tailwindcss/src/public/resolve-config.js' { 89 | import { TailwindConfig } from 'tailwindcss/tailwindconfig.faketype'; 90 | 91 | export default function resolveConfig(tailwindConfig: TailwindConfig): TailwindConfig; 92 | } 93 | 94 | --------------------------------------------------------------------------------