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/clean-moles-open.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"postcss-rule-unit-converter": minor
"postcss-pxtrans": minor
---

新增可选的 CSS 变量 fallback 单位转换,保护变量名、字符串与 URL,并保持默认行为和 pxtrans 平台转换兼容性。
4 changes: 3 additions & 1 deletion apps/website/src/guide/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ Use `propList` for property-level control, `selectorBlackList` for selectors, an

`postcss-rule-unit-converter` uses CSS tokens by default: signed/scientific values
and complete decoded units are supported, while strings, URLs, identifiers, and
the whole `var()` fallback remain untouched. An explicit `unitRegex` replaces
the whole `var()` fallback remain untouched. Set `transformVarFallbacks: true` to
convert fallback dimensions recursively while preserving variable names, strings,
and URLs. An explicit `unitRegex` overrides the fallback option and replaces
that protection. The first matching rule wins even when it returns `undefined`,
and conversions do not chain in one run; repeated processing may convert again.
See the [API reference](/plugins/postcss-rule-unit-converter/api#matching-boundaries).
Expand Down
3 changes: 2 additions & 1 deletion apps/website/src/zh-CN/guide/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ Tailwind 输出 `rem` 时,将 `postcss-rem-to-responsive-pixel` 放在 Tailwin

`postcss-rule-unit-converter` 默认使用 CSS token,支持带符号数值、科学计数法和
完整解码后的单位,保护字符串、URL、标识符及整个 `var()` fallback。
显式 `unitRegex` 会替代这些保护。第一条命中规则即为最终选择,即使它返回
设置 `transformVarFallbacks: true` 可递归转换 fallback,同时继续保护变量名、
字符串和 URL。显式 `unitRegex` 会替代这些保护和 fallback 选项。第一条命中规则即为最终选择,即使它返回
`undefined`;单轮不连锁转换,重复处理则可能继续转换。详见
[API 参考](/zh-CN/plugins/postcss-rule-unit-converter/api#匹配边界)。

Expand Down
8 changes: 8 additions & 0 deletions packages/postcss-pxtrans/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,19 @@ RN eject:
- `replace`: `boolean`, when `false` it appends a new declaration
- `mediaQuery`: `boolean`, convert px in `@media`
- `minPixelValue`: `number`, values lower than this are not converted (Harmony still converts to `ch`)
- `transformVarFallbacks`: `boolean`, default `false`; convert fallback dimensions using CSS tokens while keeping variable names, strings, and URLs intact
- `onePxTransform`: `boolean`, whether to convert `1px` (Harmony still converts `PX` to `ch`)
- `exclude`: `Array<string | RegExp> | (filePath?: string) => boolean`, skip the whole file when matched

> Note: internally this plugin reuses utilities from `postcss-plugin-shared` for prop matching and unit regex building.

With `transformVarFallbacks: true`, `var(--size48px, 48px)` becomes
`var(--size48px, 96rpx)` for `designWidth: 375`. This mode preserves platform unit
case rules and unsigned `rootValue(input, m, value)` callback arguments while
handling complete signed/scientific values. Negative zero may normalize to zero.
The default `false` retains the legacy regex path. This option does not change
`postcss-rem-to-responsive-pixel`.

### Choosing Between `exclude`, `selectorBlackList`, and `propList`

- `exclude`: skip the whole file before any declaration is processed
Expand Down
7 changes: 7 additions & 0 deletions packages/postcss-pxtrans/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,18 @@ RN eject:
- `replace`: `boolean`,`false` 时追加新声明
- `mediaQuery`: `boolean`,是否转换 `@media` 中的 px
- `minPixelValue`: `number`,小于该值时不转换(Harmony 仍会转 `ch`)
- `transformVarFallbacks`:`boolean`,默认 `false`;使用 CSS token 转换 fallback,保护变量名、字符串和 URL
- `onePxTransform`: `boolean`,是否转换 1px(Harmony 下 `PX` 仍转 `ch`)
- `exclude`:`Array<string | RegExp> | (filePath?: string) => boolean`,命中直接跳过整文件

> 说明:插件内部复用了 `postcss-plugin-shared` 的通用能力(propList 匹配、单位正则构建等)。

开启 `transformVarFallbacks: true` 后,`designWidth: 375` 会把
`var(--size48px, 48px)` 转成 `var(--size48px, 96rpx)`。单位大小写与平台规则、
`rootValue(input, m, value)` 的无符号回调参数保持原约定,同时完整处理带符号数字
和科学计数法;负零可能规范化为零。默认 `false` 保留旧正则路径。此选项不改变
`postcss-rem-to-responsive-pixel`。

### `exclude`、`selectorBlackList`、`propList` 怎么选

- `exclude`:文件级跳过,命中后整个文件都不处理
Expand Down
18 changes: 14 additions & 4 deletions packages/postcss-pxtrans/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const defaults = {
replace: true,
mediaQuery: false,
minPixelValue: 0,
transformVarFallbacks: false,
}

const deviceRatio: NonNullable<PxTransformOptions['deviceRatio']> = {
Expand Down Expand Up @@ -288,6 +289,7 @@ function plugin(userOptions: PxTransformOptions = {}) {
const preserveUnit = unConvertTargetUnit ?? targetUnit
const unitPrecision = opts.unitPrecision ?? defaults.unitPrecision
const minPixelValue = opts.minPixelValue ?? defaults.minPixelValue
const transformVarFallbacks = opts.transformVarFallbacks ?? defaults.transformVarFallbacks
const isBlacklisted = hasSelectorBlackList
? createSelectorBlacklistMatcher(opts.selectorBlackList as readonly (string | RegExp)[], { cache: true })
: undefined
Expand All @@ -296,6 +298,10 @@ function plugin(userOptions: PxTransformOptions = {}) {
{
from: unit => unit === 'px' || unit === 'rpx',
transform(value, context) {
// tokenizer 解码单位用于规则匹配,平台原有大小写和 rpx 范围仍按原始拼写判断。
if (transformVarFallbacks && !transUnits.includes(context.rawUnit)) {
return undefined
}
if (context.prop && !satisfyPropList(context.prop)) {
return undefined
}
Expand All @@ -318,7 +324,7 @@ function plugin(userOptions: PxTransformOptions = {}) {
}
}

if (!onePxTransform && value === 1) {
if (!onePxTransform && Math.abs(value) === 1) {
return isHarmony
? {
value,
Expand All @@ -327,7 +333,7 @@ function plugin(userOptions: PxTransformOptions = {}) {
: undefined
}

if (value < minPixelValue) {
if (Math.abs(value) < minPixelValue) {
return isHarmony
? {
value,
Expand All @@ -336,8 +342,11 @@ function plugin(userOptions: PxTransformOptions = {}) {
: undefined
}

// 正负号参与数值运算,用户回调继续接收旧正则提供的无符号匹配参数。
const match = transformVarFallbacks ? context.match.replace(/^[+-]/, '') : context.match
const rawValue = transformVarFallbacks ? context.rawValue.replace(/^[+-]/, '') : context.rawValue
return {
value: value / getRootValue(context.input)(context.input, context.match, context.rawValue),
value: value / getRootValue(context.input)(context.input, match, rawValue),
unit: targetUnit,
}
},
Expand All @@ -353,7 +362,8 @@ function plugin(userOptions: PxTransformOptions = {}) {
replace: opts.replace ?? defaults.replace,
rules,
unitPrecision,
unitRegex: pxRegex(transUnits),
transformVarFallbacks,
unitRegex: transformVarFallbacks ? undefined : pxRegex(transUnits),
}) as Plugin

return {
Expand Down
2 changes: 2 additions & 0 deletions packages/postcss-pxtrans/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ export interface PxTransformDeviceRatio {
* }
*/
export interface PxTransformOptions {
/** 转换 var() fallback 内的完整长度 token;默认 false,保留既有正则行为。 */
transformVarFallbacks?: boolean
/**
* Target platform.
*
Expand Down
4 changes: 4 additions & 0 deletions packages/postcss-pxtrans/test-d/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ const options: PxTransformOptions = {
expectAssignable<PxTransformOptions>(options)
pxtrans(options)
expectError(pxtrans({ platform: 'web' }))

pxtrans({ transformVarFallbacks: true })
pxtrans({ transformVarFallbacks: false })
expectError(pxtrans({ transformVarFallbacks: 'true' }))
64 changes: 64 additions & 0 deletions packages/postcss-pxtrans/test/var-fallbacks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { Input } from 'postcss'
import { transform } from './utils'

describe('pxtrans 可选的 var fallback 转换', () => {
const enabled = { transformVarFallbacks: true }

it('默认不改变既有行为,开启后按设计稿尺寸转换 fallback', () => {
const source = '.a{width:var(--nutui-48px,48px);height:48px}'
expect(transform(source)).toBe('.a{width:var(--nutui-48px,48px);height:48rpx}')
expect(transform(source, enabled)).toBe('.a{width:var(--nutui-48px,48rpx);height:48rpx}')
expect(transform(source, { ...enabled, designWidth: 375 }))
.toBe('.a{width:var(--nutui-48px,96rpx);height:96rpx}')
expect(transform(source, { ...enabled, transformVarFallbacks: false })).toBe(transform(source))
})

it('保留原始单位大小写、rpx 平台范围、完整 token 边界和符号', () => {
const source = '.a{margin:var(--x,-2px +.5px 1e2px 2Px 2PX 2pX 2rpx item2px 2pxfoo)}'
expect(transform(source, { ...enabled, designWidth: 375 }))
.toBe('.a{margin:var(--x,-4rpx +1rpx 200rpx 2Px 2PX 2pX 2rpx item2px 2pxfoo)}')
expect(transform('.a{width:var(--x,2rpx)}', { ...enabled, platform: 'h5', rootValue: 2 }))
.toBe('.a{width:var(--x,1rem)}')
})

it('保持 rootValue 回调的无符号匹配参数和真实 Input', () => {
const rootValue = vi.fn((_input: Input, _match: string, _value: string) => 2)
expect(transform('.a{margin:var(--x,-2px +.5px)}', { ...enabled, rootValue }))
.toBe('.a{margin:var(--x,-1rpx +0.25rpx)}')
expect(rootValue.mock.calls.map(call => call.slice(1))).toEqual([['2px', '2'], ['.5px', '.5']])
expect(rootValue.mock.calls[0]?.[0]).toMatchObject({ file: expect.stringContaining('input.css') })
})

it('使用绝对值判断负数阈值和一像素保留,并保持舍入精度', () => {
expect(transform('.a{margin:var(--x,-1px -2px -.5px 2px)}', { ...enabled, designWidth: 375, onePxTransform: false, minPixelValue: 2 }))
.toBe('.a{margin:var(--x,-1px -4rpx -.5px 4rpx)}')
expect(transform('.a{margin:var(--x,-1.005px .00001px -0px)}', { ...enabled, rootValue: 1, unitPrecision: 2 }))
.toBe('.a{margin:var(--x,-1.01rpx 0rpx 0rpx)}')
})

it('复用属性、选择器、文件排除和禁用指令', () => {
const source = '.a{width:var(--x,2px);height:var(--x,2px)}.skip{width:var(--x,2px)}'
expect(transform(source, { ...enabled, propList: ['width'], selectorBlackList: ['.skip'] }))
.toBe('.a{width:var(--x,2rpx);height:var(--x,2px)}.skip{width:var(--x,2px)}')
expect(transform(source, { ...enabled, exclude: () => true })).toBe(source)
expect(transform(`/* postcss-pxtrans disable */${source}`, enabled))
.toBe(`/* postcss-pxtrans disable */${source}`)
})

it('保留 Harmony 特殊单位及黑名单语义', () => {
expect(transform('.a{margin:var(--x,-1px -2px 2PX)}', { ...enabled, platform: 'harmony', designWidth: 375, onePxTransform: false }))
.toBe('.a{margin:var(--x,-1ch -4px 2ch)}')
expect(transform('.a{margin:var(--x,-2px 2PX)}', { ...enabled, platform: 'harmony', selectorBlackList: ['.a'] }))
.toBe('.a{margin:var(--x,-2ch 2ch)}')
})

it('只生成一份 replace:false 结果,并保持 mediaQuery 开关', () => {
const source = '@media (width:var(--x,2px)){.a{width:var(--x,3px)}}'
expect(transform(source, { ...enabled, replace: false, mediaQuery: true }))
.toBe('@media (width:var(--x,2rpx)){.a{width:var(--x,3px);width:var(--x,3rpx)}}')
expect(transform('.a{width:var(--x,8px)}', { ...enabled, platform: 'rn', rootValue: 2, replace: false }))
.toBe('.a{width:var(--x,8px);width:var(--x,4px)}')
expect(transform(source, enabled))
.toBe('@media (width:var(--x,2px)){.a{width:var(--x,3rpx)}}')
})
})
7 changes: 6 additions & 1 deletion packages/postcss-rule-unit-converter/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type PostcssUnitConverter = PluginCreator<UserDefinedOptions>
interface UserDefinedOptions {
rules?: readonly ConversionRule[]
unitRegex?: RegExp
transformVarFallbacks?: boolean
unitPrecision?: number
minValue?: number
keepZeroUnit?: boolean
Expand All @@ -36,6 +37,7 @@ interface UserDefinedOptions {

- `rules`: ordered conversion rules. First match wins.
- `unitRegex`: opt out of default CSS tokenization and use a custom regex instead.
- `transformVarFallbacks`: convert dimensions after the first direct comma in `var()` (default `false`). Ignored when `unitRegex` is supplied.
- `unitPrecision`: decimal precision for generated values.
- `minValue`: global minimum absolute source magnitude to convert (default `0`). Negative lengths remain eligible.
- `keepZeroUnit`: keep `0px`/`0rem` style output instead of collapsing to bare `0`.
Expand Down Expand Up @@ -151,7 +153,10 @@ scientific notation, unit case, and CSS escapes follow CSS token boundaries.
Strings, comments, and URL contents remain untouched, including escaped quotes
and uppercase `URL()`. The whole `var()` function is skipped at every nesting
depth, including its fallback; lengths in other functions such as `calc()` and
`min()` are processed normally.
`min()` are processed normally. With `transformVarFallbacks: true`, fallback
dimensions are converted recursively while the variable name, strings, and URLs
remain intact. This does not resolve variables or select their fallback. An
unclosed `var()` stays unchanged and does not call conversion callbacks.

An explicit `unitRegex` replaces tokenization entirely, including those protections.
The expression must expose the numeric value as capture group 1 and the unit as
Expand Down
6 changes: 6 additions & 0 deletions packages/postcss-rule-unit-converter/API.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type PostcssUnitConverter = PluginCreator<UserDefinedOptions>
interface UserDefinedOptions {
rules?: readonly ConversionRule[]
unitRegex?: RegExp
transformVarFallbacks?: boolean
unitPrecision?: number
minValue?: number
keepZeroUnit?: boolean
Expand All @@ -36,6 +37,7 @@ interface UserDefinedOptions {

- `rules`:有序转换规则,先命中的先生效。
- `unitRegex`:关闭默认 CSS tokenizer,改用自定义正则匹配。
- `transformVarFallbacks`:转换 `var()` 直属第一个逗号之后的 fallback,默认 `false`;显式 `unitRegex` 优先,此时该选项不参与处理。
- `unitPrecision`:输出值的小数精度。
- `minValue`:全局源值绝对值阈值,默认 `0`;负长度也可转换。
- `keepZeroUnit`:保留 `0px`、`0rem` 这类带单位的零值输出,而不是折叠成裸 `0`。
Expand Down Expand Up @@ -147,6 +149,10 @@ CSS 转义都按 CSS token 边界处理。字符串、注释与 URL 内容保持
转义引号和大写 `URL()`。默认完整跳过任意嵌套深度的 `var()` 及其 fallback;
`calc()`、`min()` 等其他函数中的长度正常转换。

开启 `transformVarFallbacks: true` 后递归转换 fallback 中的长度,仍保留变量名、
字符串和 URL;不解析变量、不选择 fallback。未闭合的 `var()` 保持原文,
不会触发其中的转换回调。

显式 `unitRegex` 会完全替代 tokenizer 及上述保护。正则必须把数字放在第 1 个
捕获组、单位放在第 2 个捕获组;缺少 `g` 标志时会自动补上,确保每个匹配项都会
处理。捕获到的单位会转成小写用于规则匹配;转义、带符号数字、科学计数法、
Expand Down
8 changes: 5 additions & 3 deletions packages/postcss-rule-unit-converter/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ The rule engine uses CSS tokenization by default. It treats signed/scientific
numbers as complete values, matches case-insensitive decoded units, and protects
strings, URLs, identifiers, and the entire `var()` fallback at any depth.
Callbacks receive signed values, and `minValue` compares absolute magnitude.
If your old configuration relied on regex substring matches or converting inside
`var()`, review [matching boundaries](./API.md#matching-boundaries) and explicitly
configure `unitRegex` only when that behavior is intended.
To convert fallback lengths safely, set `transformVarFallbacks: true`. Variable
names, strings, and URLs remain protected; variables are not resolved. If your old
configuration relied on regex substring matches, review
[matching boundaries](./API.md#matching-boundaries) and use `unitRegex` only when
that behavior is intended; it overrides the fallback option too.

## When To Migrate

Expand Down
8 changes: 5 additions & 3 deletions packages/postcss-rule-unit-converter/MIGRATION.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@

规则引擎默认使用 CSS tokenizer,完整解析带符号数字与科学计数法,解码单位并按
小写匹配,保护字符串、URL、标识符及任意深度的整个 `var()` fallback。
回调收到带符号数值,`minValue` 按绝对值比较。旧配置若依赖正则子串匹配或
转换 `var()` 中的内容,请查看 [匹配边界](./API.zh-CN.md#匹配边界),仅在确实
需要时显式配置 `unitRegex`。
回调收到带符号数值,`minValue` 按绝对值比较。
需要转换 `var()` fallback 时,优先设置 `transformVarFallbacks: true`,仍保护变量名、
字符串和 URL,不解析变量。依赖正则子串匹配时,请查看
[匹配边界](./API.zh-CN.md#匹配边界),仅在确实需要时显式配置 `unitRegex`;
自定义正则仍完整覆盖 fallback 选项。

## 什么时候该迁移

Expand Down
9 changes: 6 additions & 3 deletions packages/postcss-rule-unit-converter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,8 @@ const result = await postcss([

Advanced matching options:

- `unitRegex`: replace default tokenization with your own matching regex, for example to opt into matching inside `var(...)`
- `transformVarFallbacks`: opt into converting `var()` fallback dimensions while protecting the variable name, strings, and URLs (default `false`)
- `unitRegex`: replace default tokenization with your own matching regex; this remains authoritative even with `transformVarFallbacks: true`
- `keepZeroUnit`: keep `0px`, `0rem`, or custom zero outputs instead of collapsing them to bare `0`

## Matching And Processing Boundaries
Expand All @@ -266,7 +267,9 @@ Signed numbers and scientific notation are parsed as complete values. A rule for
`rem` does not match the unit `remfoo` in `2remfoo` or the identifier `foo2rem`.
Strings and URLs remain untouched, including escaped quotes and uppercase `URL()`.
The entire `var()` function is skipped at every nesting depth; other functions
such as `calc()` and `min()` can contain convertible dimensions.
such as `calc()` and `min()` can contain convertible dimensions. Set
`transformVarFallbacks: true` to convert fallback dimensions at any depth without
resolving the variable or choosing a fallback. An unclosed `var()` stays unchanged.

Callbacks receive the signed value: `-2rem` passes `-2`, and `1e3rem` passes
`1000`. Global and per-rule `minValue` compare the absolute source magnitude, so
Expand All @@ -292,7 +295,7 @@ property, value, and `!important`, preserving declarations with different priori
- For `presets.viewportPresetGroup()`, choose `viewportUnit: 'vw'` or `viewportUnit: 'vh'` explicitly when you want one axis.
- Prefer `definePreset(...)` and `definePresetGroup(...)` when authoring reusable presets in your own package or app config.
- Use `RuleContext.rawUnit`, `RuleContext.rawValue`, and `RuleContext.match` when your transform depends on original casing or exact matched text.
- Use `unitRegex` when you need non-default matching behavior such as not skipping `var(...)` fallbacks.
- Use `transformVarFallbacks: true` to convert fallback lengths safely; reserve `unitRegex` for custom matching semantics.
- Use `keepZeroUnit` when downstream tooling expects `0` to keep its explicit unit.
- If you want more scenario-based examples, see the cookbook.

Expand Down
Loading
Loading