Skip to content

Commit e414446

Browse files
committed
docs: tailwindcss-patch add how it works
1 parent 8e8a3dc commit e414446

File tree

1 file changed

+294
-0
lines changed

1 file changed

+294
-0
lines changed
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
---
2+
id: 20230630
3+
title: 'How to get the context of tailwindcss at runtime?'
4+
date: 2023-06-30
5+
description: 'The core principle of `tailwindcss-patch`'
6+
authors:
7+
- icebreaker
8+
tags:
9+
- 'tailwindcss'
10+
- 'context'
11+
- 'runtime'
12+
- 'plugin'
13+
- 'postcss'
14+
---
15+
16+
# How to get the context of tailwindcss at runtime?
17+
18+
> The core principle of `tailwindcss-patch`
19+
20+
- [How to get the context of tailwindcss at runtime?](#how-to-get-the-context-of-tailwindcss-at-runtime)
21+
- [Preface](#preface)
22+
- [`tailwindcss` context object](#tailwindcss-context-object)
23+
- [How to get the context outside `tailwindcss`?](#how-to-get-the-context-outside-tailwindcss)
24+
- [Patch tailwindcss](#patch-tailwindcss)
25+
- [`tailwindcss/lib/processTailwindFeatures.js`](#tailwindcsslibprocesstailwindfeaturesjs)
26+
- [`tailwindcss/lib/plugin.js`](#tailwindcsslibpluginjs)
27+
- [Get the context API](#get-the-context-api)
28+
- [Tips on writing packaged plugins](#tips-on-writing-packaged-plugins)
29+
- [Summary](#summary)
30+
31+
## Preface
32+
33+
When we use `tailwindcss` for development, we often struggle with the problem of how to redevelop `tailwindcss`.
34+
35+
Because currently `tailwindcss` only can extract the corresponding class name from our source code and generate the corresponding `css`, but can not modify the `html`/`js` package result generated by the source code.
36+
37+
What is this meaning?
38+
39+
For example I write a class named: `md:text-[20px]`, some non-`h5` platforms may not support characters like `:`, `[`, `]`, so I want to escape it while compiling.
40+
41+
`md:text-[20px]` => `md_text-_20px_`
42+
43+
Or maybe we want to obfuscate all the class names generated by `tailwindcss`
44+
45+
- `md:text-[20px]` => `tw-a`
46+
- `text-[20px]` => `tw-b`
47+
- `text-xs` => `tw-c`
48+
49+
This kind of functionality cannot be done by `plugin` and `preset` of `tailwindcss` alone, and a single `postcss` plugin can neither. We have to work with a packaging tool (`webpack`/`vite`...) to get the currently running `tailwindcss` context in the packaging tool so that all `html`/`js`/`css` files can be dynamically changed during build time.
50+
51+
## `tailwindcss` context object
52+
53+
Through debugging, we know that `tailwindcss` context will be an object constructed at runtime, which contains mainly the following fields.
54+
55+
![Image](https://pic4.zhimg.com/80/v2-6d0b87b5f472fc932b6071304e6b6d4d.png)
56+
57+
It contains some core methods of `tailwindcss` and holds some data, etc.
58+
59+
## How to get the context outside `tailwindcss`?
60+
61+
Usually, we want to get the context in the `webpack/vite plugin` so that we can make changes to our code based on some key information in the context.
62+
63+
However, `tailwindcss` itself is mostly used as a `postcss` plugin, so how do we get a `postcss` plugin to communicate with the `webpack/vite/gulp plugin`?
64+
65+
When I looked through the source code, I found that `tailwindcss` itself is great as a plugin. However, the source code was rather closed and could not expose the context for other code to use.
66+
67+
After reading and studying the source code, I came up with a solution to expose the context to other code, i.e. modify the `tailwindcss` source code to expose the context while keeping all the original functionality: put a `patch` on the running `tailwindcss` code.
68+
69+
## Patch tailwindcss
70+
71+
As we want to patch `tailwindcss`, we need to know where exactly `tailwindcss` is running in our local code.
72+
73+
We install version `3.2.2` of `tailwindcss` (the latest version as of 20230630) and go to `node_modules/tailwindcss/package.json`
74+
75+
```jsonc
76+
{
77+
"name": "tailwindcss",
78+
"version": "3.3.2",
79+
"description": "A utility-first CSS framework for rapidly building custom user interfaces",
80+
"license": "MIT",
81+
"main": "lib/index.js",
82+
"types": "types/index.d.ts",
83+
// ...
84+
}
85+
```
86+
87+
By the `package.json#main` field, we know that most of the currently running code is in the `lib` directory
88+
89+
Reading through the source code and looking through the `lib` directory, I came to the following conclusion, we need to make a change to the
90+
91+
- `tailwindcss/lib/processTailwindFeatures.js`
92+
- `tailwindcss/lib/plugin.js`
93+
94+
These `2` files should be modified as follows.
95+
96+
### `tailwindcss/lib/processTailwindFeatures.js`
97+
98+
```diff
99+
function processTailwindFeatures (setupContext) {
100+
return function (root, result) {
101+
const {
102+
tailwindDirectives,
103+
applyDirectives
104+
} = (0, _normalizeTailwindDirectives.default)(root);
105+
(0, _detectNesting.default)()(root, result);
106+
// Partition apply rules that are found in the css
107+
// itself.
108+
(0, _partitionApplyAtRules.default)()(root, result)
109+
const context = setupContext({
110+
tailwindDirectives,
111+
applyDirectives,
112+
registerDependency (dependency) {
113+
result.messages.push({
114+
plugin: 'tailwindcss',
115+
parent: result.opts.from,
116+
...dependency
117+
})
118+
},
119+
createContext (tailwindConfig, changedContent) {
120+
return (0, _setupContextUtils.createContext)(tailwindConfig, changedContent, root)
121+
}
122+
})(root, result)
123+
if (context.tailwindConfig.separator === '-') {
124+
throw new Error("The '-' character cannot be used as a custom separator in JIT mode due to parsing ambiguity. Please use another character like '_' instead.")
125+
}
126+
(0, _featureFlags.issueFlagNotices)(context.tailwindConfig);
127+
(0, _expandTailwindAtRules.default)(context)(root, result);
128+
// Partition apply rules that are generated by
129+
// addComponents, addUtilities and so on.
130+
(0, _partitionApplyAtRules.default)()(root, result);
131+
(0, _expandApplyAtRules.default)(context)(root, result);
132+
(0, _evaluateTailwindFunctions.default)(context)(root, result);
133+
(0, _substituteScreenAtRules.default)(context)(root, result);
134+
(0, _resolveDefaultsAtRules.default)(context)(root, result);
135+
(0, _collapseAdjacentRules.default)(context)(root, result);
136+
(0, _collapseDuplicateDeclarations.default)(context)(root, result)
137+
+ return context
138+
}
139+
}
140+
```
141+
142+
This file just adds a line at the end of the `processTailwindFeatures` method to give `context` to `return` out.
143+
144+
### `tailwindcss/lib/plugin.js`
145+
146+
```diff
147+
'use strict'
148+
149+
Object.defineProperty(exports, '__esModule', {
150+
value: true
151+
})
152+
const _setupTrackingContext = /* #__PURE__ */_interop_require_default(require('./lib/setupTrackingContext'))
153+
const _processTailwindFeatures = /* #__PURE__ */_interop_require_default(require('./processTailwindFeatures'))
154+
const _sharedState = require('./lib/sharedState')
155+
const _findAtConfigPath = require('./lib/findAtConfigPath')
156+
function _interop_require_default (obj) {
157+
return obj && obj.__esModule
158+
? obj
159+
: {
160+
default: obj
161+
}
162+
}
163+
164+
+ const contextRef = {
165+
+ value: []
166+
+ }
167+
module.exports = function tailwindcss (configOrPath) {
168+
return {
169+
postcssPlugin: 'tailwindcss',
170+
plugins: [_sharedState.env.DEBUG && function (root) {
171+
console.log('\n')
172+
console.time('JIT TOTAL')
173+
return root
174+
}, function (root, result) {
175+
+ // clear context each time
176+
+ contextRef.value.length = 0
177+
let _findAtConfigPath1
178+
// Use the path for the `@config` directive if it exists, otherwise use the
179+
// path for the file being processed
180+
configOrPath = (_findAtConfigPath1 = (0, _findAtConfigPath.findAtConfigPath)(root, result)) !== null && _findAtConfigPath1 !== void 0 ? _findAtConfigPath1 : configOrPath
181+
const context = (0, _setupTrackingContext.default)(configOrPath)
182+
if (root.type === 'document') {
183+
const roots = root.nodes.filter(node => node.type === 'root')
184+
for (const root of roots) {
185+
if (root.type === 'root') {
186+
- (0, _processTailwindFeatures.default)(context)(root, result);
187+
+ contextRef.value.push((0, _processTailwindFeatures.default)(context)(root, result))
188+
}
189+
}
190+
return
191+
}
192+
- (0, _processTailwindFeatures.default)(context)(root, result);
193+
+ contextRef.value.push((0, _processTailwindFeatures.default)(context)(root, result))
194+
}, false && function lightningCssPlugin (_root, result) {
195+
const postcss = require('postcss')
196+
const lightningcss = require('lightningcss')
197+
const browserslist = require('browserslist')
198+
try {
199+
const transformed = lightningcss.transform({
200+
filename: result.opts.from,
201+
code: Buffer.from(result.root.toString()),
202+
minify: false,
203+
sourceMap: !!result.map,
204+
inputSourceMap: result.map ? result.map.toString() : undefined,
205+
targets: typeof process !== 'undefined' && process.env.JEST_WORKER_ID
206+
? {
207+
chrome: 106 << 16
208+
}
209+
: lightningcss.browserslistToTargets(browserslist(require('../package.json').browserslist)),
210+
drafts: {
211+
nesting: true,
212+
customMedia: true
213+
}
214+
})
215+
let _result_map
216+
result.map = Object.assign((_result_map = result.map) !== null && _result_map !== void 0 ? _result_map : {}, {
217+
toJSON () {
218+
return transformed.map.toJSON()
219+
},
220+
toString () {
221+
return transformed.map.toString()
222+
}
223+
})
224+
result.root = postcss.parse(transformed.code.toString('utf8'))
225+
} catch (err) {
226+
if (typeof process !== 'undefined' && process.env.JEST_WORKER_ID) {
227+
const lines = err.source.split('\n')
228+
err = new Error(['Error formatting using Lightning CSS:', '', ...['```css', ...lines.slice(Math.max(err.loc.line - 3, 0), err.loc.line), ' '.repeat(err.loc.column - 1) + '^-- ' + err.toString(), ...lines.slice(err.loc.line, err.loc.line + 2), '```']].join('\n'))
229+
}
230+
if (Error.captureStackTrace) {
231+
Error.captureStackTrace(err, lightningCssPlugin)
232+
}
233+
throw err
234+
}
235+
}, _sharedState.env.DEBUG && function (root) {
236+
console.timeEnd('JIT TOTAL')
237+
console.log('\n')
238+
return root
239+
}].filter(Boolean)
240+
}
241+
}
242+
module.exports.postcss = true
243+
+ // export contexts
244+
+ module.exports.contextRef = contextRef
245+
246+
```
247+
248+
In this file, we create a `contextRef` object, `push` the context of `tailwindcss` into `contextRef.value`, export `contextRef` in the file, and clean up `contextRef.value` to avoid memory leaks.
249+
250+
### Get the context API
251+
252+
At this point, we'll be able to create a method to get it
253+
254+
```js
255+
function getContexts() {
256+
const twPath = require.resolve('tailwindcss')
257+
258+
const distPath = path.dirname(twPath)
259+
260+
let injectFilePath = path.join(distPath, 'plugin.js')
261+
262+
const mo = require(injectFilePath)
263+
if (mo.contextRef) {
264+
return mo.contextRef.value as any[]
265+
}
266+
return []
267+
}
268+
```
269+
270+
On success, we get the following information, where we want to get the names of all the generated tool classes from this field.
271+
272+
![Image](https://pic4.zhimg.com/80/v2-76820b3a5464b24d53c7a70be21e9a95.png)
273+
274+
## Tips on writing packaged plugins
275+
276+
According to this method above, the `tailwindcss` context to be fetched is usually executed after `postcss`/`postcss-loader` to get the full data object.
277+
278+
So we can get the context at a later lifecycle, like `processAssets` of `webpack` `compilation`, or at the next `loader` after `postcss-loader` is executed.
279+
280+
Also `vite/rollup` can make some changes to the code using `hooks` that are executed later, such as `generateBundle`.
281+
282+
## Summary
283+
284+
Inspired by [`ts-patch`](https://www.npmjs.com/package/ts-patch), I wrote [`tailwindcss-patch`](https://www.npmjs.com/package/tailwindcss-patch), and summarized all of the above into this package. Its main function is to make changes to the `tailwindcss/lib` source code, then expose the context and provide some handy tool classes.
285+
286+
It does version checking internally via `semver` so that different versions of `tailwindcss` are patched with different strategies.
287+
288+
In this way, I also implemented [tailwindcss-mangle](https://github.com/sonofmagic/tailwindcss-mangle), it's an obfuscator tool for tailwindcss.
289+
290+
and also implemented [weapp-tailwindcss](https://github.com/sonofmagic/weapp-tailwindcss/blob/main/README_en.md), a tool that brings `tailwindcss` to `weapp`, a non-`h5` runtime environment.
291+
292+
However, the last thing I have to say is that I wish the official `tailwincss` team would provide some way to get the runtime context in the external code. That would be much better than the less stable way I have.
293+
294+
Thanks for reading this far!

0 commit comments

Comments
 (0)