The core principle of
tailwindcss-patch
When we use tailwindcss
for development, we often struggle with the problem of how to redevelop tailwindcss
.
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.
What is this meaning?
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.
md:text-[20px]
=> md_text-_20px_
Or maybe we want to obfuscate all the class names generated by tailwindcss
md:text-[20px]
=> tw-a
text-[20px]
=> tw-b
text-xs
=> tw-c
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.
tailwindcss
context objectThrough debugging, we know that tailwindcss
context will be an object constructed at runtime, which contains mainly the following fields.
It contains some core methods of tailwindcss
and holds some data, etc.
tailwindcss
?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.
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
?
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.
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.
As we want to patch tailwindcss
, we need to know where exactly tailwindcss
is running in our local code.
We install version 3.3.2
of tailwindcss
(the latest version as of 20230630) and go to node_modules/tailwindcss/package.json
{
"name": "tailwindcss",
"version": "3.3.2",
"description": "A utility-first CSS framework for rapidly building custom user interfaces",
"license": "MIT",
"main": "lib/index.js",
"types": "types/index.d.ts",
// ...
}
By the package.json#main
field, we know that most of the currently running code is in the lib
directory
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
tailwindcss/lib/processTailwindFeatures.js
tailwindcss/lib/plugin.js
These 2
files should be modified as follows.
tailwindcss/lib/processTailwindFeatures.js
function processTailwindFeatures (setupContext) {
return function (root, result) {
const {
tailwindDirectives,
applyDirectives
} = (0, _normalizeTailwindDirectives.default)(root);
(0, _detectNesting.default)()(root, result);
// Partition apply rules that are found in the css
// itself.
(0, _partitionApplyAtRules.default)()(root, result)
const context = setupContext({
tailwindDirectives,
applyDirectives,
registerDependency (dependency) {
result.messages.push({
plugin: 'tailwindcss',
parent: result.opts.from,
...dependency
})
},
createContext (tailwindConfig, changedContent) {
return (0, _setupContextUtils.createContext)(tailwindConfig, changedContent, root)
}
})(root, result)
if (context.tailwindConfig.separator === '-') {
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.")
}
(0, _featureFlags.issueFlagNotices)(context.tailwindConfig);
(0, _expandTailwindAtRules.default)(context)(root, result);
// Partition apply rules that are generated by
// addComponents, addUtilities and so on.
(0, _partitionApplyAtRules.default)()(root, result);
(0, _expandApplyAtRules.default)(context)(root, result);
(0, _evaluateTailwindFunctions.default)(context)(root, result);
(0, _substituteScreenAtRules.default)(context)(root, result);
(0, _resolveDefaultsAtRules.default)(context)(root, result);
(0, _collapseAdjacentRules.default)(context)(root, result);
(0, _collapseDuplicateDeclarations.default)(context)(root, result)
+ return context
}
}
This file just adds a line at the end of the processTailwindFeatures
method to give context
to return
out.
tailwindcss/lib/plugin.js
'use strict'
Object.defineProperty(exports, '__esModule', {
value: true
})
const _setupTrackingContext = /* #__PURE__ */_interop_require_default(require('./lib/setupTrackingContext'))
const _processTailwindFeatures = /* #__PURE__ */_interop_require_default(require('./processTailwindFeatures'))
const _sharedState = require('./lib/sharedState')
const _findAtConfigPath = require('./lib/findAtConfigPath')
function _interop_require_default (obj) {
return obj && obj.__esModule
? obj
: {
default: obj
}
}
+ const contextRef = {
+ value: []
+ }
module.exports = function tailwindcss (configOrPath) {
return {
postcssPlugin: 'tailwindcss',
plugins: [_sharedState.env.DEBUG && function (root) {
console.log('\n')
console.time('JIT TOTAL')
return root
}, function (root, result) {
+ // clear context each time
+ contextRef.value.length = 0
let _findAtConfigPath1
// Use the path for the `@config` directive if it exists, otherwise use the
// path for the file being processed
configOrPath = (_findAtConfigPath1 = (0, _findAtConfigPath.findAtConfigPath)(root, result)) !== null && _findAtConfigPath1 !== void 0 ? _findAtConfigPath1 : configOrPath
const context = (0, _setupTrackingContext.default)(configOrPath)
if (root.type === 'document') {
const roots = root.nodes.filter(node => node.type === 'root')
for (const root of roots) {
if (root.type === 'root') {
- (0, _processTailwindFeatures.default)(context)(root, result);
+ contextRef.value.push((0, _processTailwindFeatures.default)(context)(root, result))
}
}
return
}
- (0, _processTailwindFeatures.default)(context)(root, result);
+ contextRef.value.push((0, _processTailwindFeatures.default)(context)(root, result))
}, false && function lightningCssPlugin (_root, result) {
const postcss = require('postcss')
const lightningcss = require('lightningcss')
const browserslist = require('browserslist')
try {
const transformed = lightningcss.transform({
filename: result.opts.from,
code: Buffer.from(result.root.toString()),
minify: false,
sourceMap: !!result.map,
inputSourceMap: result.map ? result.map.toString() : undefined,
targets: typeof process !== 'undefined' && process.env.JEST_WORKER_ID
? {
chrome: 106 << 16
}
: lightningcss.browserslistToTargets(browserslist(require('../package.json').browserslist)),
drafts: {
nesting: true,
customMedia: true
}
})
let _result_map
result.map = Object.assign((_result_map = result.map) !== null && _result_map !== void 0 ? _result_map : {}, {
toJSON () {
return transformed.map.toJSON()
},
toString () {
return transformed.map.toString()
}
})
result.root = postcss.parse(transformed.code.toString('utf8'))
} catch (err) {
if (typeof process !== 'undefined' && process.env.JEST_WORKER_ID) {
const lines = err.source.split('\n')
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'))
}
if (Error.captureStackTrace) {
Error.captureStackTrace(err, lightningCssPlugin)
}
throw err
}
}, _sharedState.env.DEBUG && function (root) {
console.timeEnd('JIT TOTAL')
console.log('\n')
return root
}].filter(Boolean)
}
}
module.exports.postcss = true
+ // export contexts
+ module.exports.contextRef = contextRef
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.
At this point, we'll be able to create a method to get it
function getContexts() {
const twPath = require.resolve('tailwindcss')
const distPath = path.dirname(twPath)
let injectFilePath = path.join(distPath, 'plugin.js')
const mo = require(injectFilePath)
if (mo.contextRef) {
return mo.contextRef.value as any[]
}
return []
}
On success, we get the following information, where we want to get the names of all the generated tool classes from this field.
According to this method above, the tailwindcss
context to be fetched is usually executed after postcss
/postcss-loader
to get the full data object.
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.
Also vite/rollup
can make some changes to the code using hooks
that are executed later, such as generateBundle
.
Inspired by ts-patch
, I wrote 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.
It does version checking internally via semver
so that different versions of tailwindcss
are patched with different strategies.
In this way, I also implemented tailwindcss-mangle, it's an obfuscator tool for tailwindcss.
and also implemented weapp-tailwindcss, a tool that brings tailwindcss
to weapp
, a non-h5
runtime environment.
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.
Thanks for reading this far!