Skip to content

更新日志

English · 简体中文

0.7.0 — 2026-08-11

JSON 配置

  • 命令行现在接受 adaptive.config.json,编译前会移除 $schema 元数据;JSON 损坏或顶层不是对象时,会给出清晰错误而不暴露内部调用栈。
  • 发布的 JSON Schema 已描述 $schema,编辑器可以用与文档站相同的选项模型补全并校验 JSON 配置。
  • JSON 文件通过 readFileJSON.parse 读取,继续支持 Node 18;需要正则或判断函数时仍可使用 JavaScript 配置。

质量门禁

  • npm run check 与 CI 新增 ESLint、Prettier、VitePress 源码类型检查、文档站构建,以及更严格的覆盖率阈值。
  • 新增基于比值的性能预算,用编译器耗时对比 PostCSS 自身的解析与打印耗时,避免共享 runner 上不稳定的绝对毫秒限制。
  • 补齐求值器、选择器扫描器、CLI、Schema、stdin、兼容性与错误路径测试;覆盖率达到语句 98.10%、分支 93.59%、行 99.35%。
  • 新检查发现并修复两处真实问题:通过不受支持的路径序列化 value-parser 节点,以及未知值可能在生成产物中被写成 [object Object]

0.6.0 — 2026-08-11

断点终于有了自己的画布

  • 新增 media 路由通道。 一份响应式样式表,是一个文件里装着两份设计稿:手机端那些数字量自 750 的稿子,@media (min-width: 1024px) 里那些量自 1440 的稿子。CSS 里没有任何地方写着这件事,而在此之前也没有任何地方能写。routes: [{ media: { minWidth: 1024 }, profile: 'pc' }] 把这段断点交还给它原本那份设计稿。
  • 这不是「差一点」,所以它值一个特性而不是一句备注。750 画布、流体区间上界 600px 时,@media (min-width: 1024px) { .hero { padding: 40px } } 编译出的是 clamp(17.07px, 5.33vw, 32px)——而这条规则只在 1024px 以上生效,那已经越过画布停止缩放的地方,所以在它生效的每一个宽度上,clamp() 早就顶死在上界了。这个 padding 永远是 32px。编译器跑过了,产物看着也像编译过,可没有一个值动过。
  • 匹配靠的是蕴含关系,不是文本。 { minWidth: 1024 } 认领的是「不可能在 1024px 以下生效」的规则,因此 screen and (min-width: 1200px)(min-width: 1024px) and (max-width: 1600px) 都算数,嵌套也天然成立——嵌套本来就是「且」。拿 params 当字符串比对会漏掉那个 screen and;而画布是「这条规则够得到哪些宽度」的事实,不是「这个查询是怎么拼写的」的事实。
  • 媒体查询里的 remem 一律按 16px 折算,既不看 rootValue,也不看根元素的字号。查询在任何声明能改动 font-size 之前就要求值,因此它不能依赖它自己所筛选的那一层层叠:哪怕样式表里 html 写着 62.5%64rem 也还是 1024px。这是关于查询本身的事实,不是关于页面的假设;而只认 px 会让所有 Tailwind 与 UnoCSS 项目——它们的断点全写成 rem——变成读不懂的东西。断点连续性检查用的是同一个解析器,覆盖面一并扩到了这里。
  • 编译器读不懂的查询——带逗号、带 not、带 only,或者任何非宽度特性——谁都不认领。这是「拒绝作答」,不是「全都匹配」:按一个没人核对过的条件去改派规则,正是画布错误产生的方式,而不是被抓住的方式。@container 同样从不参与计数;它约束的是元素,而 vw 从来就与元素无关。
  • selector 路由仍然高于宽度区间:组件库在任何视口宽度下都画在它自己那张画布上,跨过一个断点并不改变这个组件出自哪份设计稿。一条路由可以同时写上两者——{ selector: ['.van-'], media: { minWidth: 1024 }, profile: 'pc' }——这才是「这个组件在桌面断点处被重画了」的说法。
  • 空区间({})和反向区间(minWidth: 1024, maxWidth: 600)都是配置错误,直接抛出。前者匹配样式表里的每一条规则,等于用一种更慢的方式改 defaultProfile;后者一条都匹配不到。两者看上去都像能用的配置,所以都不能留给用户从产物里去发现。

死区告警

  • 上面这些你一条都不用先知道,也能发现问题。 只要一条规则确实换算了长度、而它的生效区间又整个落在所属画布的流体区间之外,编译器就会说出来,把两段区间的数字都报出来,并给出能修好它的那条路由。这是算术而不是启发式——两个区间压根不相交——而它正是多画布模型本来要防的那种失败,只不过是从模型唯一没盯住的那道门进来的。
  • 每个文件里,同一张画布配同一段区间只报一次,而且只对真的换算了东西的规则报。一段只改 displaycolor 的断点是再普通不过的 CSS,里面没有长度,也就无所谓常量不常量;对这些报警会把真正该看的那条淹掉。第一版就是这么干的,那条测试正是从这里写出来的。
  • 建议的修法会随情况变:当画布是被 selector 路由定下的,消息会改成让你写 { selector: […], media: … }——因为光一条 media 路由会输给 selector 路由,照做等于什么都没做。不管用的建议,比没有建议更糟。
  • appPcPreset 现在不只写自己的断点,也读它。 这个预设本来就宣称「桌面设计稿从 768px 起接管」——那个数字设定了两个 profile 的 query,所以 @adaptive pc 块能被正确地包上媒体查询。它只是从来不媒体查询,于是手写的 @media (min-width: 768px) 块仍旧按手机画布编译:预设自己跟自己打架。现在两个方向都路由了,而且 max-width 那一半是显式写出来的、没有留给 defaultProfile 兜底——这样即便有人把预设摊进一份桌面优先的配置里,这一对路由说的仍然是它字面上说的那件事。
  • 新增两个一致性用例 breakpoints/media-routebreakpoints/dead-band,覆盖「认领什么、拒绝什么,以及同一段区间只报一次」。

选择器路由

  • 修正 :not():has() 的参数在决定规则落到哪块画布.page-hero:not(.van-cell) 修饰的是页面元素——恰恰是那些不是 Vant cell 的元素——却因为文本里某处出现了 .van-cell 这串字符而被送去 Vant 的 375 画布。这类规则里每一个长度都会变成应有尺寸的整两倍,而且是静默的。路由现在读的是选择器的主体:not():has() 的参数指的是被修饰元素之外的另一个元素,在匹配之前就被摘掉;:is() / :where() / :matches() / -*-any() 的参数保留,因为那些确实是主体的候选项。在 11 份已发布组件库样式表、22,761 条规则上实测,其中 1,035 条含 :not():has():画布归属变化——库 CSS 的自身前缀总是落在被摘掉的部分之外。这个修复不付任何代价,堵上的是应用代码会一头撞进去的洞。
  • :is() / :where() 内部跨画布的列表现在会告警。 此前文档把它写成「查不出来」。:is(.van-cell, .page-hero) { padding: 16px } 是一条声明想要两个画布,和逗号列表是同一个问题、只深了一层括号,而它一直在无声通过。
  • 告警会说出拆分要付什么代价,是算出来的而不是丢给读者::is() 以其最高的那一支的特异度匹配每一支,所以把分支拆开只有在它们本来就一致时才是免费的。不一致时,告警指明降幅——:is() matches every branch at its highest, 1-1-0, so ".page-hero" would drop to 0-1-0。这才让建议可执行;一个照做之后会悄悄改变层叠的告警,是会被学会无视的告警。每条规则只报一次:剩下的列表成因相同、修法相同。
  • 新增 src/core/selectors.ts,零依赖:splitSelectorListroutingSelectornestedSelectorListsspecificitycompareSpecificityformatSpecificitysplitIsSpecificityNeutral。拆分识别字符串与属性选择器,[data-x="a)b"], .c 会正确拆成两支——只数括号会拆错。特异度按 Selectors Level 4 计算,含 :where() 计零、:is() / :not() / :has() 取最高分支、:nth-child(n of S) 计一个伪类加 S 的最高分支,以及四个单冒号历史写法的伪元素(:before:after:first-line:first-letter)按元素计。新增 40 条单元测试。

浏览器特性支持审计

  • :has() 纳入审计。 它是日常在用的选择器里最新的一个,四个引擎之间隔了好几年——Chrome 105(2022 年 8 月)、Safari 15.4,而 Firefox 要到 121(2023 年 12 月)。这是表里首尾差距最大的一行,而且失败发生在选择器层面:整条规则作废。一份在 Chrome 和 Safari 上验收过的样式表,在旧一点的 Firefox 里可能正无声地少着若干条规则。编译器不产出 :has(),它从你自己的 CSS 或组件库进来、原样穿过这一趟——而这正是「读产物」的审计能看见它的原因。与原生嵌套不同,它在文本里毫不含糊,所以是识别出来的,不是猜的。
  • scripts/capture-compat.mjs 加入 css-has 并重新烘焙进 src/core/compat-data.ts;仍然不引入任何运行时依赖。

文档站

  • 文档以站点形式发布,中英双语,地址 https://moresyl.github.io/postcss-adaptive-matrix/。它是从仓库本身构建的,而不是从仓库的一份拷贝:srcDir 就是仓库根目录,所以 docs/README.md 里那条指向 ../conformance/README.md 的链接,在站点上和在 GitHub 上是同一个道理,没有一个文件因此挪位置。搜索、暗色模式、编辑本页、最后更新时间一并到位。
  • 唯一需要设计而不是配置的,是语言切换。VitePress 按页面改写后的位置解析相对链接,于是中文页顶部那句 [English](./README.md)——在 GitHub 上完全正确——在站点上会解回中文页自己。所以链接改为在仓库里按源文件路径解析,输出为绝对站内地址。页面再怎么挪,链接也断不了。
  • 全部配置项以数据形式发布/schema/options.json:一份 JSON Schema 2020-12 文档,含每个配置项的类型、允许值、取值范围与默认值。像「precision 是不是整数、上限多少」这种问题,散文不是合适的载体。
  • 有两件事让它不至于沦为摆设。它的属性表按源接口做了类型标注,所以存在却没被描述的配置项会让 tsc 失败,被描述却已经不存在的同样如此。默认值则是在生成这份文件时从 resolveOptions() 里读出来的,不是抄的——代码里的默认值一变,这里在同一个提交里跟着变。还有一条测试补上了类型系统看不见的那一环:配置参考里印着的默认值,和上面这个是不是同一个。它当场查出一处不一致——unitToConvert 解析后是列表,而两份参考表都写着 'px'
  • 中英文都装在同一份 Schema 里:description 是英文,x-description-zh 是中文。类型不是翻译。x-also 则标注那些只有 JavaScript 能写、JSON 表达不了但配置文件接受的形式——正则、判断函数。
  • 编译器就跑在读者自己的浏览器里。 插件只有一个运行时依赖,链路上没有任何 Node API,所以发布出去的那份源码被直接引进试验场页面,PostCSS 在客户端运行。背后没有服务,也没有任何需要跟着版本同步的东西。配置栏按 JavaScript 表达式求值而不是按 JSON 解析,因为值得一试的东西有一半是 JSON 写不出来的。六个示例,每个对应一个真的有人问的问题,其中一个专门演示会触发告警的配置。
  • 每种语言各有一份 llms.txtllms-full.txt,遵循 llms.txt 约定;每一页的原始 Markdown 也在「页面路径 + .md」上直接提供——于是「把这一页交给模型」是一次抓取,而不是把渲染后的 HTML 再刮回文本。大纲上方的三个按钮用的就是它:复制为 Markdown、查看原始 Markdown、把这一页带进对话。
  • 新增 docs/agents.md / docs/agents.zh-CN.md,把这些入口集中到一页,并写清提示词里该有什么:一个默认「全局只有一个设计稿宽度」的 Agent,写出来的配置能编译,但是错的。
  • .github/workflows/docs.yml 在推送到 main 时部署。tsconfig.json 现在也覆盖 docs/.vitepress,站点自己的源码和其余代码一起做类型检查。

文档

  • 新增 scripts/check-docs.mjs,接进 npm run check,因而也进了 CI。它校验每个本地链接可达、每个 #锚点 对得上真实标题,以及每一页都有另一种语言的对应页、并且在顶部链接过去。真正划算的是最后一条:两种语言是分别写的而不是翻译的——这既是它们读起来顺的原因,也是其中一份可以悄悄不存在的原因。它当场查出两处:CODE_OF_CONDUCT 只有中文内容却挂着英文文件名,示例 README 把两种语言堆在同一个文件里。
  • 锚点检查按 /\r?\n/ 切行。Windows 上这里每个文件都是 CRLF,行尾的 \r 会让非多行模式下的 $ 匹配不上,而 . 也不匹配它——于是直接按 '\n' 切会一条标题都找不到,把仓库里所有锚点全报成坏的。这不是假想,这是这个检查第一版的真实表现。
  • examples/app-pc 新增 adaptive.config.mjs,单独导出配置本身;postcss.config.mjs 改为 import 它。两个 runner 要的形状不同——PostCSS 要 { plugins: [...] },CLI 要配置本身——而把画布写两遍正是两边最后描述出两张不同设计稿的方式。示例 README 现在给的是真能跑起来的命令,含 --targets

性能

  • routingSelector 对不含 : 的选择器立即返回,而绝大多数选择器都不含;同时这道还原挪进了 forSelector 内部——在「压根不做选择器路由」的那道判断之后。配了 libraries: false 的项目现在为这个特性一分钱都不用付。utility-framework 语料上的编译器耗时:4.38ms → 3.58ms。

一致性套件

  • 原子化用例补入 space-x-4divide-y-2。此前整个套件里一个功能性伪类都没有,而这两个是日常工具类。同一个工具类,三种毫不相干的真实形状:Tailwind 4 把整体裹进 :where(...),UnoCSS wind3 写成扁平的 > :not([hidden]) ~ :not([hidden]),UnoCSS wind4 直接产出带 &原生嵌套。现在同样的 2px 在三者中都得到一模一样的 clamp(1.70667px, 0.53333vw, 2.56px)border 的细线三者都保住,wind4 的原生嵌套原样写回。
  • 新增 test/idempotence.test.ts把编译产物再编译一遍,什么都不该变,九种配置 × 八份样式表。一致性套件本来就对每个用例断言了这一点,但只在那个用例自己声明的选项下断言——没被覆盖的是那些会改变产物形状的开关之间的组合,而第二趟要扛住的正是形状。第二趟不是假想:一个发布预编译 CSS 的包,会再经过使用方应用的构建流水线;monorepo 里先编译共享组件库、再编译引用它的应用,也是同一回事。
  • 真正需要钉死的是原子化模式配上静态字号。原子化会把 rem 加进 unitToConvert,而文字通常写成 rem + vw——那个 vw 就是告诉第二趟「这个值已经编译过了」的记号。fontFluidity: 0 之下没有 vw32px 变成光秃秃的 2rem,下一趟会把它当成设计稿长度再换算一次。它扛住了,但没有任何东西在守着它——rootValue 两端用的是同一个,写的时候 ÷16、读的时候 ×16,互为精确的逆运算,这个值是它自己的不动点。这是算术的性质,不是谁写下来的规则;这道除法的任意一端一挪,失败都是静默的:不报错、不告警,只是每存一次盘文字就小一点。

类型

  • 所有取数组的选项现在都接受 readonly 数组:routeslibrariestextPropertiespropListselectorExcludevalueExcludeincludeexcluderoot.injectTo,以及路由与库定义里的 file / selector / property / prefix / tokenPrefixunitToConvert 本来就接受,这让 API 自相矛盾:用 as const 写的配置——在 TypeScript 里这是最自然的写法——除了那一个字段之外每个都报类型错误。

0.5.0 — 2026-08-10

文档

  • 全部描述文件改为中英双语X.md 为英文、X.zh-CN.md 为中文,每页顶部互链。覆盖 README、docs 全部 11 篇、一致性套件说明、贡献指南、安全策略与本文件。英文不是把中文机翻一遍——同一件事在两种语言里该由不同的句子承担,所以两版是分别写的。
  • 英文版配套的图放在 docs/assets/en/,是按英文重画的:中文标签短,直接替换会撑出卡片,tracks the viewport 还会被虚线从下面穿过。
  • 画布模型图移除第三方库名。模型与是哪家库无关,「移动端组件库 / 375 设计稿」「桌面端组件库 / 无设计稿 · 真实像素」说的是同一件事,而整张图从头到尾只在讲本编译器自己。

浏览器特性支持审计与降级

  • 新增 auditCompatibility(css, targets):给一串「浏览器 + 你要支持的最低版本」,逐条列出产物里超出目标的语法、不支持时丢掉的东西、以及关掉它的开关。审计读的是编译产物文本而不是配置——这是让审计和输出永远不会走偏的唯一做法,经预设、经组件库路由、经手写 CSS 进来的特性一样能看见。同时导出 detectFeaturesCOMPAT_FEATURESFEATURE_SUPPORTcompatFeature,类型齐全。
  • 命令行新增 --targets "safari 14, ios_saf 13",在对照表之后输出 needs 段。四行的顺序是有意的:先说丢什么,再说换成什么——「iOS Safari 13 太老了」单独拿出来没法行动,而 CSS 支持缺口真正要紧的一直是跟着一起消失的有多少。CSS 不报错,它丢弃:值读不懂丢一条声明,选择器读不懂丢一整条规则,@ 规则读不懂丢一整块,全程无声。特性表因此按「丢得多」排序,不按「谁更新」。
  • 覆盖 11 项:@layer:where()@container / 容器单位、clamp() / min() / max()vi、逻辑属性、var()env(safe-area-inset-*)vw、原生嵌套。原生嵌套本编译器不产出(读进来的嵌套原样写回),列入是因为它是会被问到的问题,而靠模式识别它会把普通 CSS 误读成嵌套——报错比不报更糟。
  • 版本数据由 scripts/capture-compat.mjs 从 caniuse-lite 烘焙进 src/core/compat-data.ts,caniuse-lite 只是 devDependency,插件不增加任何运行时依赖,离线可审计。「某特性从哪个版本开始能用」是不会再变的历史;使用率则故意不看——0.4% 的用户算不算数是关于你的项目的决定,browserslist 已经是回答那个问题的地方。取的是「从此再没断过支持」的版本而非第一个出现 y 的版本(个别特性发布后被撤回过)。
  • --targets 收显式的名字与版本,不收 browserslist 查询:查询要拉进 browserslist 包,回答的是关于用户的问题而不是关于这份样式表的问题,而且同一条查询会随数据库更新而改变含义——代码一个字没动,下个月构建就红了。不认识的目标名报错并以 1 退出,不静默跳过:被悄悄丢掉的目标比没有审计更糟,因为它读起来像通过了。
  • caniuse 没有 :where()vi 的独立条目,改用 :is()svh/lvh/dvh 条目,并在数据里标出这是代理而非实测。两组都出自同一节规范、同批发布(:where() / :is():Chrome 88、Firefox 78、Safari 14)。
  • 新增 root.logical(预设字段 rootLogical):设为 false 时基础样式改写 width / margin-left / margin-right / max-width,而不是 inline-size / margin-inline / max-inline-size这是审计过程中发现的真实缺口——逻辑属性是编译器产出的语法里唯一一个失败之后页面看起来还正常的:丢掉 margin-inline: auto,列宽完全正确、贴在屏幕左边;丢掉 max-inline-size,列铺满整屏。两种都不像故障,因而比一眼可见的崩塌更容易活着上线,而此前没有任何开关能避开它。横排页面上两种拼法等价,关掉不损失任何东西。预设同时新增 rootLayer,透传到 layer——同样是支持开关,不该为了够到它而放弃预设。
  • 文档新增浏览器特性支持与降级:特性 × 浏览器最低版本表、逐项的「谁产出它 / 丢什么 / 怎么关、代价是什么」,以及这份审计不能代替真机测试的边界(它只能证明目标浏览器解析得了这份 CSS 的语法;渲染差异、软键盘、地址栏是别的问题)。反过来,版本门槛这件事真机也测不了——手上那台 iOS 17 读得懂 @layer,对「15.4 以下读不懂」没有任何说明力。

断点检查

  • 修正默认预设上稳定误报两条 shrinksroot.fixedContainingBlock 会把固定元素的 left: 0 改写成 left: var(--adaptive-root-gutter),而这个留白本来就该在断点处跳变——列宽从 480 换到 1920,留白从 143.96px 正确地掉到 0。读成设计长度是倒退,读成它本身是修正在起作用。比较前把这个变量代成 0px 而不是整条跳过,left: calc(clamp(…) + var(--adaptive-root-gutter)) 里的 clamp() 照样接受检查。在默认配置上就狂叫的检查,是会被学会跳过的检查。

原子化 CSS(Tailwind / UnoCSS)

  • 修正工具类一个都换算不到:这是静默的——手写 CSS 被缩放、工具类原样保留,两套尺寸从此对不齐,不报错。两个大版本各卡在一处。旧版(Tailwind 3、UnoCSS presetUno / presetWind3)把长度写成 rem,而编译器只读 px;新版(Tailwind 4、UnoCSS presetWind4)把长度整个搬进主题 token,.p-4 编译成 padding: calc(var(--spacing) * 4),工具类里根本没有长度可读,而自定义属性默认不换算。
  • unitToConvert 现在接受数组,一趟读多种单位。这不是锦上添花:这两个框架同一张产物里两种单位都有——间距和字号是 rem,而边框宽度、p-[13px] 这类方括号任意值是 px,各自描述同一张设计稿。只读 rem 会漏掉所有边框,只读 px 会漏掉所有间距,两种单选都是错的。remrootValue 折算成像素,其余单位按面值读取。
  • 修正 unitToConvert: 'rem' 一直把 1.5rem 当成 1.5 像素:换算前不折算,于是低于 minPixelValuehairline 阈值而被整批跳过。这两道阈值现在一律按像素判定——0.0625rem1px 是同一根细线,怎么写的不影响它有多细。
  • 新增 rootValue(默认 16),读写两头共用一把尺:既决定 rem 输入折合多少像素,也决定文字静态项写成多少 remhtml { font-size: 62.5% } 的项目配 rootValue: 10 即可。
  • 新增 withAtomicCss(base, options?)包装而非替换现有配置,把 rem 补进 unitToConvert,并认领主题 token 前缀 --spacing--text---leading---radius---container-。认领源头即可,工具类不用动——calc(clamp(a, b, c) * 4) 恒等于 clamp(4a, 4b, 4c)(正系数下乘法可穿过 clamp),产物与直接换算 16px 逐位相同。故意不认领三类:--breakpoint-* 是画布切换的宽度,缩放它等于移动断点本身;--tracking-*em 发布,依附的字号已被做成流体,再缩一次是叠加;--shadow-* 的像素是按屏幕尺度画的层次感。自定义长度族用 tokenPrefixes 补。
  • 默认 textProperties 加入 --text-*--leading-*:字号被发布成 token 时,名字不长得像字体属性,漏掉就意味着这个字号失去浏览器缩放。这一条只决定一个已经要换算的长度怎么写,不决定它换不换算,因此对未被认领的 token 不起作用。
  • 新增一致性用例 atomic/{tailwind-v4,unocss-wind3,unocss-wind4},输入是三者的真实产物原文(Tailwind 4.3.3、UnoCSS 66.7.5 两个预设),用 scripts/capture-atomic.mjs 重抓。两个大版本的产物形状差别大到手写必然写成想象中的样子。这两个框架不设为 devDependency:抓下来的 CSS 就是全部输入,把 npm test 绑在别人的发版节奏上换不来额外信息。
  • 路由的 property 通道收到正则时立即报 TypeError 并说明正确写法。另两个通道都收正则,这里只收字符串前缀,此前的表现是运行中途 prefix.toLowerCase is not a function

破坏性(类型)ResolvedAdaptiveMatrixOptions.unitToConvertstring 变为 string[]AdaptiveMatrixOptions.unitToConvert 放宽为 string | readonly string[],传字符串照旧。findContinuityIssues 新增可选第二参数 rootFontSize

编译器

  • 修正跨画布的文字尺寸:项目画布与组件库画布不一致时,该库的文字全错。普通长度两边一直是一致的(都归结为 值 ÷ 画布),文字不是——文字保留一段固定的 rem 以便浏览器缩放仍然有效,而这段固定长度此前锚在各自的画布上。Vant 画在 375、页面画在 750 时,两者描述的是同一份设计的两套单位,Vant 的 16px 与页面的 32px 本是同一个尺寸,却在 390px 视口下分别渲染成 16.22px 与 26.62px——小了约 40%,而 375 与 1440 上都看不出来。750 稿的项目配 Vant 是国内移动端最常见的组合之一;antd-mobile 的 1x/2x 双份产物同理。
  • 新增 profile 字段 textAnchorWidth:文字静态部分锚定的宽度,默认等于 designWidth。组件库画布一律继承所属 profile 的锚点,因此无需配置。等价形式是「先把长度换算成锚点画布的单位,再照常套公式」:流体项恰好不变(P × F / D 与画布同比例约掉),只有静态项被归一化。非文字长度 fontFluidity = 1、静态项恒为 0,产物逐字节不变,strategy: 'viewport' 同样不受影响。
  • 新增性质测试「同一份设计换一张画布尺寸不变」:随机画布与随机缩放比下,V px on DV×k px on D×k 在每个视口上必须一致。一致性套件只能覆盖有人想到要写下来的画布组合,而这个 bug 恰好只在两张画布同时出现时暴露。

编译器与校验

  • @adaptive <画布> 落在一个没有声明 query 的 profile 上时会告警。按文件夹分双端的项目(src/mobile/**src/pc/** 各一套页面代码)通常两个 profile 都不带 query——切换本来就不由 CSS 负责。此时在共用组件里写 @adaptive pc { ... } 读起来是「这些规则给 PC 用」,编译出来却是无条件规则,而且更靠后,在任何视口都会赢,没有任何地方会说话。query: false 不告警:那是作者明确表示切换发生在 CSS 之外。指向同一张画布的 @adaptive 也不告警:没有切换,拆开不丢东西。
  • 文档新增「两套页面代码,按文件夹分」一节,写清文件路由只决定按哪张稿换算、不会附带媒体查询,以及共用组件、组件库、路由具体度这几处的取舍。

构建工具集成

  • 新增真实 Vite 构建测试:脚手架里有一个由 Vite 自行发现的 postcss.config.mjs、一份从 node_modules 引入的依赖 CSS、以及一个按 @vitejs/plugin-vue 同款方式提供的 <style> 块。此前集成文档里的每一条说法都没有任何东西验证过——而这些说法一旦不成立,构建仍然成功,只是样式表是错的。现在验证:配置被找到并生效、依赖按 node_modules 路径落到组件库画布且与页面等价尺寸逐字节相同、带 query 串的 SFC id 能被包含式 file 路由命中、二次构建产物完全一致。
  • 并且把文档里「锚定结尾的正则匹配不到 SFC」这条坑也写成了测试:用 /\.mobile\.css$/ 构建一遍,断言该 <style> 块确实静默留在默认画布上,而其余产物一字不差。这类说法只有被反向验证过才算数。

可选运行时

  • 修正 observeAdaptiveViewport 的帧泄漏:update() 是公开方法,却会把调度器的帧句柄清零。「先手动 update()、再 destroy()」这一串下来,已排队的那一帧既没被记录也没被取消,会在下一拍落到已销毁的观察者上。现在只有调度器自己清句柄,destroy() 之后也把句柄归零,重复 destroy() 不会去取消宿主已经回收再分配的句柄。
  • 补齐运行时测试:没有 visualViewport 的旧 WebView 回退、iOS 橡皮筋回弹导致的负键盘高度、逐字段的非数值读数、事件合帧、销毁时机、以及不传参数时读全局对象这条所有浏览器使用者实际走的路径。分支覆盖 72% → 100%。

组件库

  • 新增 scripts/verify-libraries.ts:下载每个内置库的已发布产物,用真实的 node_modules 路径编译,核对前缀是否真的存在、路由落到哪张画布、是否幂等、有无告警、接缝检查是否误报。此前注册表里除 Vant 外都是照文档写的,没有实证;下面三条都是这个脚本查出来的。
  • 修正 antd-mobile:该库把同一份样式表发布了两次,bundle/ 画在 375 上、2x/bundle/ 画在 750 上,类名与 token 名完全相同(实测 5.42.3,后者每个长度恰好是前者的两倍)。此前 .adm- 前缀路由会把 2x 产物按 375 换算,页面上每一个尺寸都是应有的两倍,且没有任何报错或告警。新增 antd-mobile-2x 条目,自动模式下无需配置。
  • 新增 scoped:限定 prefixtokenPrefix 只在 file 同时命中时生效。一个前缀对应两张画布时,只有路径能区分。限定路径的路由先于不限定的路由测试,因为它更具体;路径不存在时(打包器内联依赖)退回不限定的那一条。scoped 却不给 file 直接报错。
  • 移除 Varlet 的 tokenPrefix: '--var-':该库的自定义属性根本不带前缀,叫 --field-padding--icon-size-md,声明在光秃秃的 :root 上。这条规则此前匹配不到任何东西。认领这些名字等于认领 --card-width 本身,注册表只收无歧义的前缀,所以不补,改为在文档里给出显式路由的写法。
  • 文档补上实测的前缀命中率,并写明设计宽度这一列核对不了(CSS 里看不出稿子画在多宽),以及 naive-uimui 的样式在运行时生成、磁盘上没有样式表。

断点接缝检查

  • 只在至少一侧是编译器产出的公式时才报。此前会把库自己有意写下的断点差异当成接缝:Quasar 的 .q-tooltip 手机上 padding: 8px 16px、600px 往上 6px 10px,是点击区域的取舍,两个数字都是人手写的、也比对过。「倒退只可能来自跨画布」这个完备性论证只覆盖本编译器产出的公式,对原样保留的样式表不成立。一侧换算另一侧没有,仍然报。
  • 一致性套件的用例改用编译器形状的值。此前十余条负向用例写的是裸 40px,加上这道门之后会因为「没被编译」而通过,而不是因为它们各自要验的那件事。

0.4.0 — 2026-08-09

断点接缝检查

  • 新增断点接缝检查:命令行会指出「视口变宽、尺寸反而变小」的声明,并给出两侧的实际像素值。两张设计稿各自都对,接缝处未必对,而这类问题只在某一个宽度上出现——日常调试的 375 和 1440 都正常。检查也从包里导出为 findContinuityIssues(root),可用于让构建直接失败。
  • 检查按绝对值比较,且断点两侧变号一律不报。编译器输出的公式,其绝对值对视口宽度单调不减且全程不变号,所以绝对值倒退只可能来自跨断点换画布。按数值比较对每一条负长度都是反的:负外边距、外溢这类值本来就靠远离零来变大。
  • 检查会代入同一份样式表里的主题 token。组件库的尺寸几乎不写字面量——Vant 4.10.0 的 3198 条普通声明里有 1173 条完全经 var() 读取,在第一个 var( 就放弃等于避开了本插件要适配的那一层。代入只在值由视口宽度单独决定时进行:token 只声明在 :root / :host / html 上且别处没有第二份,每一处声明要么无条件、要么位于纯像素宽度的 @media 里。token 自身在断点处被改写也算一次接缝,即使消费它的规则只写了一次。实测(Vant 4.10.0 完整样式表)可求值的值分量 622 → 1309 条(17.6% → 36.9%),收录 779 个 token。
  • 实测误报:69 份一致性套件产物、上述 Vant 样式表、本仓库示例工程,shrinks 报告数均为 0。
  • 新增 evaluateLength:把编译器产出的 clamp() / min() / max() / calc() 在指定视口宽度上求值。env()%、容器单位一律返回 null 而不是猜一个数。

打包与类型

  • 修正 CommonJS 入口:require('postcss-adaptive-matrix') 此前返回的是命名空间对象,直接调用会抛 plugin is not a function,必须写 .default。而 plugins: [require('postcss-adaptive-matrix')({ ... })] 正是所有 postcss.config.js 的通用写法——本仓库 Webpack 文档里的示例自己就是错的。现在 module.exports 就是插件本身,.default 与各具名导出仍作为属性保留。
  • 修正 CommonJS 类型入口。两处:exports 里的 types 此前只有顶层一处,CJS 使用者拿到的是 ESM 版 .d.ts;而 .d.cts 本身只声明具名导出,于是 import x = require('postcss-adaptive-matrix') 报 “has no call signatures”——代码能跑、编辑器报红。现在 require 指向 .d.cts,且 .d.ctsexport = 描述真实形状,类型经合并的命名空间保留,import type { AdaptiveMatrixOptions } 照常可用。ESM 入口与类型不受影响。
  • 新增针对构建产物本身的测试:其余测试都从 src 导入,而「require 拿到什么」「类型解析到哪个文件」由构建与 package.json 决定,从源码导入永远测不到。npm run check 因此改为先构建再测试。

编译器与校验

  • 跨画布的选择器列表(如 .van-cell, .page-hero { ... })现在会告警并指出哪个选择器落空了。一条声明只能有一个结果,此前是静默按第一个命中的画布编译整条规则。
  • @adaptive pc;(没有块)此前被改写成 @media (min-width: 768px);——不是合法 CSS,而作者想放到那张画布上的规则仍留在原画布。现在告警并保持原样。
  • 命令行的 -c 不再在配置模块漏写 default、或预设忘了调用时静默改用内置默认值。这两种写法此前会跑完并打印一份看着正确的对照表,没有任何地方说过配置没被读到。
  • 新增配置校验:unitstrategy 的取值、空的 unitToConvert、与 CSS 已定义的 at-rule 重名的 atRuleName、空的 root.selector。这些字段写错的后果都是静默的,其中 unit 写错会直接产出无效 CSS。
  • 新增基于性质的随机测试:以 evaluateLength 为判据,在数百组生成的画布上验证设计宽度恒等、绝对值单调、区间外恒定、区间内线性与幂等。一致性套件只能覆盖有人想到要写下来的设计宽度。

0.3.0

  • 新增 adaptive-matrix 命令行预览:逐条声明的前后对照,--from 可在上线前验证文件路由,--css 输出完整产物且警告走 stderr。
  • 修正嵌套场景:@media / @supports / @layer / @container / @scope / @starting-style 内的声明现在会被换算,@font-face / @page / @property / @counter-style 内的长度保持原样。
  • 修正幂等性:已经带视口单位的 clamp() / min() / max() 不再被二次换算。
  • 修正根容器基础样式的幂等性:产物带 /* postcss-adaptive-matrix foundation */ 标记,再编译一次既不会把 max-inline-size: 480px 这类固定上限当成设计稿尺寸缩放,也不会叠出第二份。
  • 忽略注释(adaptive-ignore 系列)不再从产物中删除。被忽略的值没有任何自身痕迹,注释一旦消失,第二趟编译就会把作者明确排除的尺寸换算掉;注释会被压缩器去掉,不影响上线体积。
  • 一致性套件对每一个样例增加幂等断言:产物再编译一遍必须原样返回。
  • 修正重复声明:同一规则内重复书写的声明现在每一条都会换算。此前只换算第一条,而层叠中生效的是最后一条,等于整条换算失效。
  • 支持带指数的数字:1e2px 就是 100px,此前被静默跳过;min(1e2vw, 50px) 也不再被误判为「没有视口单位」。
  • 修正 at-rule 大小写:@ADAPTIVE / @Adaptive@adaptive 等价,与 CSS 对 at-keyword 的大小写不敏感一致。此前不被识别,整块会被浏览器丢弃且没有任何提示。
  • 新增 root.injectTo(预设为 rootInjectTo):限定根容器基础样式注入到哪些文件。组件化项目里每个 <style> 块都是独立文件,默认会逐个注入一份。
  • 只写排除项的 propList(如 ['!border*'])现在直接报错——它匹配不到任何属性,等于整份样式表都不换算。
  • profiles 里使用保留前缀 library: 现在直接报错并指回 libraries: [{ extends }]——此前会被合成的组件库画布静默覆盖。
  • 未知画布的告警不再列出内部合成的组件库画布,并说明浏览器会整块丢弃该 at-rule;unknownProfile: 'error' 下的报错不再建议开启已经开启的选项。
  • 新增注册表不变量测试,覆盖前缀歧义、画布取值与自动模式的短前缀策略。
  • 重写文档结构,新增快速上手、构建工具集成、组件库适配、可选运行时与命令行预览五篇,并补充 SVG 图示。

0.2.0

  • 新增组件库画布、自动识别、路由覆盖和设计令牌适配。
  • 新增 fixed 根包含块定位矫正与桌面端偏移处理。
  • 将编译核心与 PostCSS 适配层解耦,公开解析后的多画布配置能力。
  • 建立 50+ 组 conformance 固定样例、133 项测试和性能基准工具。
  • 重写多画布模型、组件库接入、架构与迁移文档。

0.1.0

  • 首次实现 App/PC 多设计画布编译模型。
  • 支持有界流体尺寸、可缩放文字、媒体查询和容器查询 profile。
  • 支持动态设计宽度、文件/属性/选择器/值过滤与忽略指令。
  • 提供可选根布局、安全区变量和 VisualViewport 运行时。
  • 发布 ESM、CommonJS 与 TypeScript 类型。

基于 MIT 协议发布