Rethinking the Future of weapp-tailwindcss

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

![image](https://blog.aitoearn.ai/content/images/2025/11/img_001-715.jpg)

Click to follow the official account for timely technical insights.

[![image](https://blog.aitoearn.ai/content/images/2025/11/img_002-662.jpg)](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 Hooks

const 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)

![image](https://blog.aitoearn.ai/content/images/2025/11/img_003-628.jpg)

**[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)**

---

Read more

Translate the following blog post title into English, concise and natural. Return plain text only without quotes. 哈佛大学 R 编程课程介绍

Harvard CS50: Introduction to Programming with R Harvard University offers exceptional beginner-friendly computer science courses. We’re excited to announce the release of Harvard CS50’s Introduction to Programming in R, a powerful language widely used for statistical computing, data science, and graphics. This course was developed by Carter Zenke.