Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
38ee7c4
fix(css): preserve user variables during generated cleanup
sonofmagic Sep 13, 2026
7a4a494
test(css): cover user variable preservation
sonofmagic Sep 13, 2026
e82055a
Merge branch 'main' into codex/ci-css-release-recovery
sonofmagic Sep 13, 2026
b6d2590
fix(css): preserve generated cleanup and framework cascade
sonofmagic Sep 13, 2026
3cc17c8
perf(css): reduce framework cascade restoration overhead
sonofmagic Sep 13, 2026
a73a7fd
perf(css): index restored framework rules
sonofmagic Sep 13, 2026
008a410
perf(css): avoid redundant framework css parsing
sonofmagic Sep 13, 2026
67776a9
perf(css): fast path large generated bundles
sonofmagic Sep 13, 2026
1e74178
perf(css): bypass duplicate filtering for large bundles
sonofmagic Sep 13, 2026
d1e2a4d
fix(css): retain small bundle overrides
sonofmagic Sep 13, 2026
5e6fd2d
perf(css): skip redundant restore for large bundles
sonofmagic Sep 13, 2026
ddf0676
ci: rerun complete validation
sonofmagic Sep 13, 2026
c31466b
test(vite): update banner cleanup snapshot
sonofmagic Sep 13, 2026
f31bc15
fix(css): preserve web Tailwind banner parity
sonofmagic Sep 13, 2026
5c1ddd6
test(vite): normalize cache banner snapshots
sonofmagic Sep 13, 2026
55217b4
fix(css): preserve framework cascade and mini-program output
sonofmagic Sep 13, 2026
eb8aaa9
docs: record final css recovery validation
sonofmagic Sep 13, 2026
5c2668c
perf(css): reduce framework rule signature allocations
sonofmagic Sep 13, 2026
0cdac28
perf(css): skip unnecessary framework composition parsing
sonofmagic Sep 13, 2026
0ade9c5
fix(css): adapt DingTalk assets and preserve empty-input composition
sonofmagic Sep 14, 2026
0d5c3dc
Merge branch 'main' into codex/ci-css-release-recovery
sonofmagic Sep 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/tidy-css-cleanup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'weapp-tailwindcss': patch
---

修复 Tailwind CSS v4 CSS 恢复与清理时误删用户自定义变量的问题:使用生成阶段的具体声明识别重复主题变量,保留用户变量的值、优先级和覆盖顺序,避免原始 rem 值覆盖已经转换的 rpx 值。

恢复框架 vendor CSS 时通过规则索引精确匹配条件上下文、选择器和完整声明,保留生成侧已有的层位置以及框架侧的后续覆盖,合并重复编码声明。大产物同样执行完整恢复,批量清理节点并共享生成主题声明索引,避免重复解析和逐条删除造成的性能开销。同时改进 Taro issue-998 配置的跨平台路径检测。

公开 core 编译入口在最终生成小程序 CSS 时也移除 Tailwind banner,避免 weapp-vite 等内置集成绕过 bundler 适配后留下 WXSS 产物检测误报。

默认产物 matcher 识别钉钉 `.ddml` 和 `.ddss`,使 Mpx 钉钉构建中的模板类名转译及 rem2rpx 样式适配正常执行。没有生成 CSS 时仍合并框架样式的编码声明并保留覆盖顺序。
14 changes: 14 additions & 0 deletions demo/__tests__/wxss-output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,13 +425,27 @@ describe('demo wxss artifacts', () => {
expect(countDuplicateSelectorRules(duplicateInjection, TAILWIND_V4_ROOT_SELECTORS)).toBe(1)
})

it.each(RAW_SNIPPETS)('检测 $label 的完整违规规则', ({ label, css }) => {
const signature = FORBIDDEN_SNIPPETS.find(snippet => snippet.label === label)!.signature
expect(getRuleSignatures(`.before{color:red}@media screen{${css}}.after{color:blue}`)).toContain(signature)
})

it('多选择器和声明必须完整匹配', () => {
const bold = FORBIDDEN_SNIPPETS.find(snippet => snippet.label === 'b/strong bold')!.signature
expect(getRuleSignatures('b, strong { font-weight: bolder; }')).toContain(bold)
expect(getRuleSignatures('.b, .strong { font-weight: bolder; }')).not.toContain(bold)
expect(getRuleSignatures('b, strong { font-weight: normal; }')).not.toContain(bold)
})

it('matches forbidden selectors exactly without treating utility classes as global tags', () => {
const globalTag = getRuleSignatures('small { font-size: 80%; }')
const utilityClass = getRuleSignatures('.small { font-size: 80%; }')
const forbiddenSignature = FORBIDDEN_SNIPPETS.find(snippet => snippet.label === 'small font size')!.signature

expect(globalTag).toContain(forbiddenSignature)
expect(utilityClass).not.toContain(forbiddenSignature)
expect(getRuleSignatures('.small:hover { font-size: 80%; }')).not.toContain(forbiddenSignature)
expect(getRuleSignatures('small, textarea { font-size: 80%; }')).not.toContain(forbiddenSignature)
})
})

Expand Down
2 changes: 1 addition & 1 deletion demo/taro-vite-react-tailwindcss-v4/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default defineConfig<'vite'>(async (merge, { command, mode }) => {
designWidth: taroPlatform.isWeb
? 750
: (input) => {
const file = typeof input?.file === 'string' ? win32.normalize(input.file) : ''
const file = typeof input?.file === 'string' ? win32.resolve(projectRoot, input.file) : ''
return win32.dirname(file) === win32.resolve(projectRoot, 'src/pages/issue-998') ? 375 : 750
},
deviceRatio: {
Expand Down
2 changes: 1 addition & 1 deletion demo/taro-webpack-react-tailwindcss-v4/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export default defineConfig<'webpack5'>(async (merge, { command, mode }) => {
designWidth: taroPlatform.isWeb
? 750
: (input) => {
const file = typeof input?.file === 'string' ? win32.normalize(input.file) : ''
const file = typeof input?.file === 'string' ? win32.resolve(projectRoot, input.file) : ''
if (win32.dirname(file) === win32.resolve(projectRoot, 'src/pages/issue-998')) {
return 375
}
Expand Down
120 changes: 120 additions & 0 deletions docs/engineering/lessons/ci-css-release-recovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
---
status: partial
issue: https://github.com/sonofmagic/weapp-tailwindcss/pull/1197
baseline: e82055a63e2eea5e049da690de1bca575aa020d3
regressions:
- packages/weapp-tailwindcss/test/bundlers/shared/generator-css/generated-cleanup.test.ts
- packages/weapp-tailwindcss/test/bundlers/shared/generator-css/user-css.test.ts
- packages/weapp-tailwindcss/test/bundlers/framework-css-composition.test.ts
- packages/weapp-tailwindcss/test/bundlers/framework-css-composition.unit.test.ts
- e2e/uni-app-vite-tailwindcss-v4-layer-output.test.ts
- packages/weapp-tailwindcss/test/ci/repoctl-release.test.ts
- demo/__tests__/wxss-output.test.ts
- e2e/taro-ci-coverage-matrix.test.ts
- e2e/taro-vite-react-tailwindcss-v4.test.ts
- e2e/template-contract.test.ts
- packages/weapp-tailwindcss/test/defaults.test.ts
---

# CSS 清理、框架组合与发布恢复回归

## 症状

- Release 的 WXSS 检查把 `.small` 工具类误认为全局 `small` 标签。
- v4 根规则的变量名前缀清理会误删用户变量;恢复原始根规则又会让未转换的 `rem` 覆盖已转换的 `rpx`。
- 框架 vendor CSS 按规则集合去重后,`16 → 32 → 16` 的最后一条覆盖消失。NutUI 的组合选择器与后续单选择器覆盖也受到影响。
- npm publish 与 tag 推送成功后,GitHub Release POST 返回 502。现有说明修复入口只能修复已有 Release。

原始失败:[34748944107](https://github.com/sonofmagic/weapp-tailwindcss/actions/runs/34748944107)、[34748944100](https://github.com/sonofmagic/weapp-tailwindcss/actions/runs/34748944100)。

## 根因与纠正

1. WXSS 禁止片段改为 PostCSS AST 签名匹配,覆盖标签、类、伪类、多选择器和全部已有 Preflight 片段。
2. 从生成阶段传递原始及转换后的声明来源,按条件上下文、属性、值和优先级清理。只有数值小数前导零被归一化;未知来源与用户覆盖保留。补齐删除声明后标记变化、单独 `:host` 的用户变量及空白格式回归。
3. 用户样式转换之后,裸选择器补偿只补没有被转换结果表示的规则,避免重新插入原始 `rem`。
4. 框架产物用于恢复用户 CSS 覆盖顺序,同时保留生成侧已经确定的层位置。早期无条件移动规则和按产物大小跳过恢复的做法均不正确,见下方补充纠正。真实生成器测试分别执行 legacy 与 graph,并精确验证用户变量、单位转换及重复规则顺序。
5. 合并多个 CSS 输入后,只保留开头的一条 `@charset`;不在构建输出目录直接写文件。
6. 模板按 semver 范围能否覆盖当前稳定版验收,不要求范围字符串一致。Taro 配置通过执行实际 AST 中的 `designWidth` 表达式验证 POSIX、Windows、相对路径、盘符和相邻目录。
7. 为实际实现包 `@icebreakers/monorepo@5.5.0` 提交 pnpm patch。Release API 的重试限定四次,遵守限流等待,POST 响应不确定时先按 tag 查询;不重跑 npm publish。
8. 显式 `release notes repair --create-missing --tag` 支持 dry-run。核对 npm 精确版本、本地与远端 tag、目标提交内的包名和版本,再复用原说明生成逻辑补建缺失 Release。

## 初次修复阶段的验证记录

本次在独立 `codex/ci-css-release-recovery` worktree 验证;以下是初次修复阶段的本地记录;最终实现的重新验证见后面的“本轮验证”。

- `CI=1 pnpm test --update=none`:540 文件、5278 测试通过;5 文件、47 测试既有跳过。
- CSS 定向集合:13 文件、288 测试通过;新增清理器、框架组合和 repoctl 集合另行复验,50 测试通过。
- `CI=1 pnpm test:release`:58 测试通过、4 跳过;附加发布插件测试 2 项通过。
- `CI=1 pnpm e2e:issues-977-978`:8 项通过。
- Taro coverage、模板范围和 canonical smoke:3 文件、20 项通过。
- `pnpm --filter weapp-tailwindcss build`:通过,真实 demo 使用本 worktree 的新构建。
- `CI=1 pnpm e2e:static --shard=1/3 --update=none`:21 文件、118 测试通过;6 文件、18 测试跳过。
- static 三个分片和 apps-generator 已完成禁止更新复核:分别 107/122/130 项通过,既有跳过 18/10/3。
- `pnpm typecheck`:未通过。当前与未修改基线各 445 条诊断,按文件和错误码对比无新增。不能据此宣称严格类型全绿。
- ESLint:使用仓库默认范围检查改动代码;对新增测试与框架组合测试额外取消忽略进行检查。全仓默认忽略的历史 demo/大型测试文件不能冒充已通过强制 lint。

## 快照语义

限定更新 Taro Vite React/Vue v4 与 uni-app Vite v4 的 static 基线,随后以 `--update=none` 验证。

- 两套 NutUI 产物按选择器、条件上下文、声明属性和优先级统计,React 6707 项、Vue 5233 项,未缺失声明;大量 diff 来自重复副本减少和保留框架已处理的格式。
- 对照 NutUI 原始 CSS,`.nut-tabs-titles-item-smile` 的 `bottom` 最后应为 `var(--nutui-tabs-titles-item-smile-bottom, -10%)`,`.nut-indicator-white .nut-indicator-line` 的 `opacity` 最后应为 `0`。旧基线的 `15%` 与 `1` 来自错误覆盖顺序;真实构建行为测试固定这两个纠正。
- uni-app 保留的 `--test-color`、`--color-test`、字体与色彩变量来自 `App.vue`,runtime 的状态栏高度和窗口偏移仍在主样式中。

## 补充纠正:大产物与层位置

- 超过 250 KB 就跳过恢复会丢失框架来源、编码声明和用户规则,不能作为性能优化。移除全部大小短路,用条件上下文、选择器、声明值、优先级和声明顺序建立 AST 索引。
- 将所有框架规则移到生成结果尾部也不正确:uni-app 的 `wx-button { background: #000 }` 和 components 层必须位于 utilities 之前。对覆盖序列保留生成侧已匹配前缀,从首次缺失处恢复框架侧后缀。
- 逐条删除 PostCSS 节点会在大数组中反复移动元素,改为每个容器一次批量重建;主题来源声明延迟解析,并在同一恢复周期共享索引。相邻完全相同的规则在同一遍历中合并,保留被其它覆盖隔开的重复声明;先复现 uni-app x 的相邻组件规则重复,再修复并保持原 static 基线通过。
- 命名层即使清空也必须保留首次声明位置,匿名层具有独立身份,不能跨输入合并。组合选择器的每一项都参与覆盖区间,避免 `.a,.b` 与 `.a` 被分别去重后颠倒顺序。两项新增用例均已先复现错误,再修复并通过。
- Taro React v4 基线变化包括去除两条重复字体定义、动画块位置调整和保留四组声明顺序不同的 vendor 规则。按条件、选择器、属性、优先级和声明值比较,更新前后均为 6388 个唯一声明,无缺失、无最终值变化;NutUI 关键覆盖的真实构建断言通过。

### 本轮验证(接续 5c1ddd676)

- `CI=1 pnpm test --update=none`:541 文件、5292 测试通过;5 文件、47 测试既有跳过。
- 清理器及组合单测包含 legacy/graph 各 8000 条填充规则;最终组合集合 14 项通过。WXSS 独立配置 34 项通过。
- 核心包构建及新增/修改源文件、组合测试 ESLint 通过。
- `pnpm typecheck` 仍有 445 条诊断,本轮修改文件无诊断;不将包构建通过等同于严格类型检查通过。
- `CI=1 pnpm test:release`:222 项通过、4 项既有跳过。
- issue-977/978:8 项通过;Taro coverage contract 7 项、模板 contract 9 项、canonical smoke 4 项通过。
- generator 的基线更新限定 Taro Vite React/Vue v4 与 uni-app Vite v4;汇总 JSON/中英文报告只同步这三个项目构建产生的字节数,选择器列表和其它项目未变。
- 最终实现的 Taro React/Vue、uni-app、uni-app x 与层顺序 E2E:5 文件、14 项通过;uni-app x 使用本机 HBuilderX Alpha 5.25.2026082902-alpha CLI 构建,没有将其视为设备验收。
- static 三个分片和完整 generator 集合已在最终 core banner 修复及工作区依赖覆盖后重新验证通过;weapp-vite 独立 banner 基线另行更新并复核。

### 验证

- 本轮完整单测、发布集合和冻结安装均通过;apps-generator 已单独更新并以禁止更新模式复核。
- 静态分片、真实框架构建和环境型类型诊断分别记录在交付说明中;快照只在确认产物语义后更新。

### 规模验证

本地预热两轮后取七轮中位数,4000/8000/16000 条规则分别约 21/38/75 ms;输入约 163/327/661 KB。此前逐条删除的大数组移动开销已经消除。该测量用于确认规模趋势,不替代框架构建与 HMR 的 CI 性能门禁。

```sh
rtk proxy pnpm exec tsx --eval 'import { performance } from "node:perf_hooks"; import { composeFrameworkProcessedCss as compose } from "./packages/weapp-tailwindcss/src/bundlers/shared/framework-css-composition.ts"; for (const size of [4000,8000,16000]) { const source=Array.from({length:size},(_,i)=>`.vendor-${i}{color:rgb(1,2,3);height:1px}`).join(""); const generated=source+".utility{display:flex}"; const times=[]; for(let i=0;i<9;i++){const start=performance.now();compose("",generated,source);if(i>1)times.push(performance.now()-start)} times.sort((a,b)=>a-b); console.log({rules:size,bytes:generated.length,medianMs:times[3]}) }'
```

## 适用边界

### Mpx 多平台及空生成输入复核

- PR run `34790213574` 的 Mpx 各平台基线仍记录原始 `.25rem` 和转换后的 `8rpx`。在 `rem2rpx: true` 配置下,旧根规则被恢复后会覆盖已转换的值;当前四个平台产物仅保留 `8rpx`,用户 `--test-color` 和 `--color-test` 保留。
- 钉钉还有独立问题:默认 matcher 不包含 `.ddml` / `.ddss`,使模板类名与样式适配跳过。补齐默认 matcher,并覆盖 POSIX、Windows 盘符、相对路径及非产物后缀。修复后真实 `.ddss` 保留 `8rpx` 和用户变量,模板类名与任意值样式消费验证通过。
- 新增空输入回归发现:生成 CSS 为空时直接连接两侧框架 CSS 会保留重复 `@charset`。撤回这一条短路;两侧框架输入为空时仍直接返回生成产物,保留性能优化。
- 两项缺陷先由新增单测复现,共 5 个失败;修复后 defaults、composition unit 和 legacy/graph 组合测试共 24 项通过。
- 基线更新命令:`CI=1 pnpm e2e:demo:matrix mpx-tailwindcss-v4:wx mpx-tailwindcss-v4:ali mpx-tailwindcss-v4:swan mpx-tailwindcss-v4:tt mpx-tailwindcss-v4:dd --update --build-only`;钉钉 matcher 修复后单独重建该目标。审查后五个基线只删除未转换的 rem 值。
- 禁止更新复验:`CI=1 pnpm e2e:demo:matrix mpx-tailwindcss-v4:wx mpx-tailwindcss-v4:ali mpx-tailwindcss-v4:swan mpx-tailwindcss-v4:tt mpx-tailwindcss-v4:dd`,五个平台全部通过生产构建、初始、替换、新增及恢复四轮 HMR;不将其视为真机证据。
- 修改后的完整 `CI=1 pnpm test --update=none`:542 文件、5302 测试通过,5 文件、47 测试既有跳过;`CI=1 pnpm test:release`:222 测试通过、4 测试既有跳过。核心包构建、修改文件 ESLint、`pnpm agents:check`、`git diff --check` 通过;`pnpm release status` 仍只有核心包中文 patch intent,联动范围由已有发布策略决定。
- `CI=1 E2E_PROJECT_FILTER='^mpx-tailwindcss-v4$' pnpm e2e:static --update=none`:64 文件、342 测试通过,15 文件、44 测试跳过。该入口仍执行共享 smoke/contract,包括 Taro H5、uni-app 层顺序和 runtime 变量;Mpx static 两项通过,无基线自动更新。

### 发布与设备边界

- 本地验证覆盖编译与产物;没有把 CLI 构建记为 IDE、设备或真机验收。
- 中文 patch intent 只指定 `weapp-tailwindcss`。清理器及组合逻辑属于核心包,`@weapp-tailwindcss/postcss` 的源码和公共行为未改动,不单独增加其版本。
- `pnpm release status` 显示核心包 5.5.5 → 5.5.6,CLI 与依赖消费包的联动来自仓库已有 fixed/dependency 策略。
- 只交付补丁和恢复测试,本次没有运行 npm publish、推送发布 tag 或实际补建 Release。
- 上游增强:[repoctl #887](https://github.com/sonofmagic/repoctl/issues/887)、[repoctl #888](https://github.com/sonofmagic/repoctl/issues/888),关联 #848,不重复提出 provenance 问题。

## 规则评估

不新增 AGENTS 规则。现有生成来源、构建图、回归与基线规则足够,本次用持久行为测试补上执行缺口。
Loading
Loading