Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .changeset/angry-wolves-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@weapp-tailwindcss/postcss": patch
"weapp-tailwindcss": patch
---

将 CSS 语法解析与 tokenize 收口到 @weapp-tailwindcss/postcss,主包不再直接依赖样式处理库
6 changes: 6 additions & 0 deletions .changeset/funny-deers-shine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@weapp-tailwindcss/postcss": patch
"weapp-tailwindcss": patch
---

将 legacy selector/unit、空块清理和 Harmony CSS style value 变换迁到 @weapp-tailwindcss/postcss
6 changes: 6 additions & 0 deletions .changeset/real-pens-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@weapp-tailwindcss/postcss": patch
"weapp-tailwindcss": patch
---

将 uni-app x 边框 preflight 与 v4 theme CSS 变换迁到 @weapp-tailwindcss/postcss
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
- `packages/weapp-tailwindcss` 的 JS 转译必须遵循 `classNameSet` 精确命中原则,禁止启发式兜底转译。
- `packages/weapp-tailwindcss` 的 bundler 适配不得依赖硬编码目录或后置 `fs.readFile` 兜底来还原源码关系;源码关系必须来自构建图、插件生命周期缓存或明确的扫描层。
- `packages/weapp-tailwindcss` 的样式注入、preflight、Tailwind 入口选择和分包样式隔离不得依赖硬编码文件名或输出路径片段;必须来自 CSS 内容、用户显式配置、构建图、loader/transform 阶段缓存或 source-candidates 元数据。
- CSS 语法解析、tokenize、selector/value parser、AST 变换和 PostCSS 管线只允许实现在 `packages/postcss`;`packages/weapp-tailwindcss` 禁止直接依赖 `postcss-scss`、`@csstools/*`、`postcss-selector-parser`、`postcss-value-parser`、`lightningcss` 或 `postcss`,必须通过 `@weapp-tailwindcss/postcss` 消费。
- demo、Web/H5、watch 与 e2e 场景都必须遵守 Tailwind CSS 由 `weapp-tailwindcss` 生成的约束,不能为修复样式或 HMR 问题注册官方 Tailwind 生成插件。
- 运行时封装(`packages-runtime/*`)改动需重点关注 escape/unescape、merge 兼容和缓存边界。
- Release 工作流发布 npm 必须使用 trusted publishing/OIDC:发布 job 使用 Node 24 以满足 npm CLI 的 OIDC 支持要求,保留 `permissions.id-token: write` 与 provenance,禁止在发布步骤注入 `NPM_TOKEN` 或 `NODE_AUTH_TOKEN`。
Expand Down
1 change: 1 addition & 0 deletions packages/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- 构建产物类型声明;
- README 中的公开用法(如有)。
- 默认保持“单一职责”包边界,不把上游业务逻辑混入基础工具包。
- CSS 语法解析、tokenize 与 AST 变换属于 `@weapp-tailwindcss/postcss`;其他包不得再引入 `postcss-scss`、`@csstools/css-tokenizer` 等样式处理依赖,应通过该包的导出消费。

## 工程与测试

Expand Down
13 changes: 11 additions & 2 deletions packages/postcss/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,23 @@
## 适用范围

- 本文件适用于 `packages/postcss`。
- 本包聚焦 CSS AST 级处理,不承担 JS/模板层的启发式纠错职责。
- 本包是仓库内 CSS 语法解析、tokenize、AST 变换和 PostCSS 管线的唯一实现位置。
- 本包聚焦 CSS AST 级处理,不承担 JS/模板层的启发式纠错职责,也不承担 bundler 生命周期或 Tailwind class 生成。

## 包内结构约定

- `src/syntax/`:CSS/SCSS 解析、`@import` specifier tokenize/quote,以及对外暴露的 syntax API。
- `src/plugins/`:插件实现与 pipeline 组装。
- `src/compat/`:版本兼容与降级逻辑。
- `src/compat/`:版本兼容与降级逻辑,包括 uni-app x 边框 preflight、style value、legacy selector/unit 与 Tailwind v4 theme source 变换
- `src/selectorParser/`:选择器解析相关能力。
- `src/utils/`:纯工具函数,保持无副作用、可单测。

## 所有权

- 新增 CSS parser/tokenizer/selector/value/compat 变换时写在本包并导出;禁止在 `packages/weapp-tailwindcss` 再实现一份。
- `postcss-scss`、`@csstools/*`、`postcss-selector-parser`、`postcss-value-parser` 只作为本包依赖。
- 主包可以通过本包 re-export 的 `postcss` 做编排级 parse/walk,但不能拥有这些 CSS 工具依赖。

## 变更原则

- 优先基于 PostCSS AST 做变换,避免对原始字符串做全局替换。
Expand All @@ -24,6 +32,7 @@
- `pnpm --filter @weapp-tailwindcss/postcss test`
- `pnpm --filter @weapp-tailwindcss/postcss exec vitest run -u`(仅在确认预期变更时)
- 针对单模块:`pnpm --filter @weapp-tailwindcss/postcss exec vitest run test/<case>.test.ts`
- 语法层回归:`pnpm --filter @weapp-tailwindcss/postcss exec vitest run test/syntax-css-import.test.ts`

## 测试补充要求

Expand Down
28 changes: 28 additions & 0 deletions packages/postcss/src/compat/legacy-css/apply.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import postcss from 'postcss'

/** 删除兼容源中的 `@apply` 规则及其空包装 at-rule。 */
export function removeTailwindApplyRules(rawSource: string) {
try {
const root = postcss.parse(rawSource)
let removed = false
root.walkAtRules('apply', (rule) => {
const parent = rule.parent
if (parent?.type === 'rule') {
parent.remove()
}
else {
rule.remove()
}
removed = true
})
root.walkAtRules((rule) => {
if (rule.nodes && rule.nodes.length === 0) {
rule.remove()
}
})
return removed ? root.toString() : rawSource
}
catch {
return rawSource
}
}
164 changes: 164 additions & 0 deletions packages/postcss/src/compat/legacy-css/dedupe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import type { Node, Rule } from 'postcss'
import postcss from 'postcss'
import {
collectGeneratedSelectors,
getRuleCompatSelectorKeys,
hasUtilityClassSelector,
isCustomPropertyOnlyRule,
isPseudoContentInitRule,
} from './selectors'

function collectGeneratedDeclarationPropsBySelector(generatedCss: string, selectors: Set<string>) {
const propsBySelector = new Map<string, Set<string>>()
try {
const generatedRoot = postcss.parse(generatedCss)
generatedRoot.walkRules((rule) => {
const matchedSelectors = getRuleCompatSelectorKeys(rule).filter(selector => selectors.has(selector))
if (matchedSelectors.length === 0) {
return
}
const props = new Set<string>()
rule.walkDecls((decl) => {
props.add(decl.prop)
})
for (const selector of matchedSelectors) {
const existing = propsBySelector.get(selector)
if (existing) {
for (const prop of props) {
existing.add(prop)
}
}
else {
propsBySelector.set(selector, new Set(props))
}
}
})
}
catch {
return propsBySelector
}
return propsBySelector
}

function isRuleCoveredByGeneratedProps(
rule: Rule,
generatedDeclarationPropsBySelector: Map<string, Set<string>>,
) {
const nodeSelectors = getRuleCompatSelectorKeys(rule)
if (nodeSelectors.length === 0) {
return false
}
const props = new Set<string>()
rule.walkDecls((decl) => {
props.add(decl.prop)
})
if (props.size === 0) {
return false
}
for (const selector of nodeSelectors) {
const generatedProps = generatedDeclarationPropsBySelector.get(selector)
if (!generatedProps) {
continue
}
if ([...props].every(prop => generatedProps.has(prop))) {
return true
}
}
return false
}

export function removeGeneratedSelectorCompatCss(css: string, generatedCss: string) {
const generatedSelectors = collectGeneratedSelectors(generatedCss)
if (generatedSelectors.size === 0) {
return css
}

try {
const root = postcss.parse(css)
let removed = false
root.walkRules((rule) => {
if (isPseudoContentInitRule(rule)) {
rule.remove()
removed = true
return
}
if (isCustomPropertyOnlyRule(rule) && !isPseudoContentInitRule(rule) && !hasUtilityClassSelector(rule.selector)) {
return
}
if (getRuleCompatSelectorKeys(rule).some(selector => generatedSelectors.has(selector))) {
rule.remove()
removed = true
}
})
root.walkAtRules((atRule) => {
if (atRule.nodes && atRule.nodes.length === 0) {
atRule.remove()
}
})
return removed ? root.toString() : css
}
catch {
return css
}
}

export function collectDedupedPostTransformCompatCss(css: string, generatedCss: string) {
const generatedSelectors = collectGeneratedSelectors(generatedCss)
if (generatedSelectors.size === 0) {
return css
}
const generatedDeclarationPropsBySelector = collectGeneratedDeclarationPropsBySelector(generatedCss, generatedSelectors)

const preservedNodes: Node[] = []
try {
const root = postcss.parse(css)
root.each((node) => {
if (node.type === 'rule') {
const nodeSelectors = getRuleCompatSelectorKeys(node)
const duplicated = nodeSelectors.some(selector => generatedSelectors.has(selector))
if (!duplicated) {
preservedNodes.push(node.clone())
return
}
if (isRuleCoveredByGeneratedProps(node, generatedDeclarationPropsBySelector)) {
return
}
if (isCustomPropertyOnlyRule(node) && !isPseudoContentInitRule(node) && !hasUtilityClassSelector(node.selector)) {
const declarationProps = new Set<string>()
node.walkDecls((decl) => {
declarationProps.add(decl.prop)
})
for (const selector of nodeSelectors) {
const generatedProps = generatedDeclarationPropsBySelector.get(selector)
if (!generatedProps) {
continue
}
for (const prop of generatedProps) {
declarationProps.delete(prop)
}
}
const nextRule = node.clone()
nextRule.walkDecls((decl) => {
if (!declarationProps.has(decl.prop)) {
decl.remove()
}
})
if (nextRule.nodes.length > 0) {
preservedNodes.push(nextRule)
}
}
return
}
preservedNodes.push(node.clone())
})
if (preservedNodes.length === root.nodes.length) {
return css
}
const nextRoot = postcss.root()
nextRoot.append(preservedNodes)
return nextRoot.toString()
}
catch {
return css
}
}
4 changes: 4 additions & 0 deletions packages/postcss/src/compat/legacy-css/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { removeTailwindApplyRules } from './apply'
export { collectDedupedPostTransformCompatCss, removeGeneratedSelectorCompatCss } from './dedupe'
export { collectGeneratedSelectors, normalizeCompatSelectors } from './selectors'
export { inheritLegacyUnitConvertedDeclarations } from './units'
Loading
Loading