;
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------