Rethinking the Future of weapp-tailwindcss
# weapp-tailwindcss: Breaking Through Development Bottlenecks

Click to follow the official account for timely technical insights.
[](https://mp.weixin.qq.com/s?__biz=MzU2NjU3Nzg2Mg==&mid=2247546851&idx=1&sn=d872a35b3a3dfb9cddaadd80880157f1&scene=21&poc_token=HFY6KWmj-s4vzjoXfxlPDBboTR8SfulhZvTkxJen#wechat_redirect)
💰 **TRAE SOLO live competition is now open** — tap the image above for details 🔍
---
## Introduction
Hello everyone — I’m the author of **weapp-tailwindcss** and **weapp-vite**.
Recently, I’ve been reflecting deeply on the future of **weapp-tailwindcss** — so much so that I’ve barely been gaming, with a brief return to *StarCraft II*.
---
## The Major Obstacle
### Persistent Problem
The main issue severely hampering **weapp-tailwindcss**'s growth is this:
> Key atomic style foundational packages — *tailwind-merge*, *class-variance-authority*, and *tailwind-variants* — do not work well in mini-program environments.
---
### Why They Fail in Mini-Programs
**Root Cause:** Mini-program **WXML** class names cannot contain many special characters, such as `!`, `[`, `]`, `#`, etc.
**Solution in weapp-tailwindcss:**
Class names are transformed **at compile time** to ensure compatibility with mini-program compilation plugins.
Example compilation transform:
- Input: `bg-[#123456]`
- Output: `bg-_h123456_` (transformed in WXML, JS, WXSS)
However, *tailwind-merge* works **at runtime**. By then, the strings are already transformed, making merging impossible or incorrect.
---
## Attempts at Compatibility
### Path 1: tailwind-merge Plugin / createTailwindMerge
**Goal:** Write a custom **weapp-tailwindcss** plugin for *tailwind-merge*.
**Process:**
- Read *tailwind-merge* source code
- Experiment with `extendTailwindMerge` and `createTailwindMerge`
- Export internal conflict tables
- Override illegal characters with custom `escape` hooks
**Failure Points:**
- *tailwind-merge* relies heavily on **runtime** string formats
- Hardcoded constants cannot be changed via configuration:
export const IMPORTANT_MODIFIER = '!' // Not allowed in mini-programs
const MODIFIER_SEPARATOR = ':' // Not allowed in mini-programs
🔗 [Source: parse-class-name.ts in tailwind-merge v3.3.1](https://github.com/dcastil/tailwind-merge/blob/v3.3.1/src/lib/parse-class-name.ts)
**Conclusion:** Runtime syntax dependence = no viable solution without rewriting or forking.
---
### Path 2: Compile-time Exemption
Idea: Skip escaping within `twMerge` / `twJoin` / `cva` at compile time, then wrap and escape the final output.
Example wrapper:
export function cn(...inputs: ClassValue[]) {
const result = twMerge(inputs)
return escape(result)
}
---
#### Issues Discovered
1. **Literal Strings** cn('bg-[#123456]', `bg-[#987654]`)
ok in isolation.
2. **Variable References** const a = 'bg-[#123456]'
cn(a, 'xx', 'yy')
3. **Concatenations & Templates** const a = 'bg-[#123456]' + ' bb' + ` text-[#123456]`
4. **Complex Interpolation** const b = 'after:xx'
const a = 'bg-[#123456]' + ' bb' + `${b} text-[#123456]`
---
**Temporary Win:** Using ASTNodePathWalker + scope.getBinding + WeakMap allowed writing v1 of `@weapp-tailwindcss/merge`.
---
#### Example User Case that Broke It
// shared2.js
export const ddd = 'bg-[#123456]'
const a = 'bg-[#123456]'
export { a as default }
// shared.js
export const a = 'bg-[#123456]'
const b = 'bg-[#123456]'
const c = 'bg-[#123456]'
const d = 'bg-[#123456]'
export default d
export { b }
export { c as xaxaxaxa }
export * from './shared2'
// main.js
import cc, { b as aa, a as bb } from './shared'
import * as shared from './shared'
cn(bb, cc, aa, shared.default, shared.a, '[]', '()')
Resolution: Not feasible without implementing a custom bundler — too costly.
---
## Realization
Both compile-time and runtime exemption approaches failed. The remaining choice:
👉 **Rewrite `merge` so escape logic lives at runtime.**
---
## Why Rewrite `merge`
- **Tailwind CSS v4** introduces new arbitrary values impossible to reconcile at compile time
- Function name blacklists can't predict all factory exports like `variants` (`tv`)
- Compile-time exemptions are fragile and break after minification/obfuscation
---
## New Design Philosophy
### 1. Unify All Entry Points
Unify `twMerge`, `twJoin`, `createTailwindMerge`, `extendTailwindMerge`, `cva`, `variants` under the same runtime transformer system.
### 2. Separate Escape and Unescape Hooksconst transformers = resolveTransformers(options)
const aggregators = {
escape: transformers.escape,
unescape: transformers.unescape,
}
---
### Bidirectional Processing Chain
Flow: **unescape → tailwind-merge → escape**const normalized = transformers.unescape(clsx(...inputs))
return transformers.escape(fn(normalized))
---
## Rethinking `@weapp-core/escape`
### Old Mapping Problem
The old many-to-one mapping made `unescape` impossible:
export const MappingChars2String: MappingStringDictionary = {
'[': '_',
']': '_',
// ...
}
**Loss of Original:** `[bg:red]` becomes `__bg_red_` — cannot tell `[` from `]`.
---
### New Reversible State Machine
Each illegal character gets a **unique mapping**:
export const MappingChars2String = {
'[': '_b',
']': '_B',
'(': '_p',
')': '_P',
'#': '_h',
'!': '_e',
'/': '_f',
'\\': '_r',
'.': '_d',
':': '_c',
'%': '_v',
',': '_m',
'\'': '_a',
'"': '_q',
'*': '_x',
'&': '_n',
'@': '_t',
'{': '_k',
'}': '_K',
'+': '_u',
';': '_j',
'<': '_l',
'~': '_w',
'=': '_z',
'>': '_g',
'?': '_Q',
'^': '_y',
'`': '_i',
'|': '_o',
'$': '_s',
} as const
**Benefit:** `unescape(escape(input))` now perfectly restores the original.
---
## Runtime Configuration
The new API allows disabling processing steps:
const { twMerge: passthrough } = create({ escape: false, unescape: false })
- **Legacy Projects:** Can gradually migrate by toggling escape/unescape
- **SSR:** Disable escaping server-side and re-enable client-side
- **Customization:** Override mappings via `map` option
---
## Release Highlights
### weapp-tailwindcss@4.7.x & @weapp-tailwindcss/merge@2.x
- Marks the **runtime era** for merging
- Unified escape/unescape chain
- Fully reversible mapping
- Configurable runtime and migration path
---
## Final Thoughts
- **Challenge:** Tailwind's evolving syntax vs mini-program constraints
- **Result:** Runtime-based merging for robust compatibility
- **Outlook:** Feedback from real projects will drive next iterations
When it comes to tailoring Tailwind CSS for China's mini-program platforms, I’m proud to be among the first.
---
## Resources
- [tailwind-merge plugin docs](https://github.com/dcastil/tailwind-merge/blob/v3.3.1/docs/writing-plugins.md)
- [NodePathWalker.ts](https://github.com/sonofmagic/weapp-tailwindcss/blob/main/packages/weapp-tailwindcss/src/js/NodePathWalker.ts)
- [ModuleGraph.ts](https://github.com/sonofmagic/weapp-tailwindcss/blob/main/packages/weapp-tailwindcss/src/js/ModuleGraph.ts)

**[Read the original article](https://juejin.cn/post/7569536278254125092)**
**[Open in WeChat](https://wechat2rss.bestblogs.dev/link-proxy/?k=292dd4ae&r=1&u=https%3A%2F%2Fmp.weixin.qq.com%2Fs%3F__biz%3DMzU2NjU3Nzg2Mg%3D%3D%26mid%3D2247547142%26idx%3D1%26sn%3D8cb3166a4af002ccb20c50a47bd6e186)**
---