Skip to content

命令行预览 ​

English · 简体中文

改一个 designWidth、挪一次 fluid 区间,要看效果就得重新构建,再去浏览器里肉眼比对。这条命令把这一步缩短成一次回车:

bash
npx adaptive-matrix src/styles/app.css
src/styles/app.css
  profiles: app (default), pc, +5 library canvases
  .page
    padding    16px → clamp(13.65333px, 4.26667vw, 20.48px)
    font-size  16px → clamp(0.94867rem, calc(0.65rem + 1.49333vw), 1.098rem)
  @media (min-width: 768px) › .page
    padding    48px → clamp(34.13333px, 3.33333vw, 64px)
  3 converted, 0 left as authored

输出的是逐条声明的前后对照,不是整份 CSS。构建工具跑出来的产物动辄几千行,而你想确认的通常只有那么几个数字。

用法 ​

adaptive-matrix <file...> [options]
adaptive-matrix [options] -- <file...>
cat app.css | adaptive-matrix --from src/app.css

可以用 - 显式表示 stdin,但不能把它与文件路径混用,否则输入位置不同就会有一份源文件被静默忽略。同理,--from 只能用于一个输入;多个文件不可能共用一个逻辑路径后仍保持各自的文件路由含义。

选项作用
-c, --config <path>默认导出插件选项的模块,或一个写着选项的 .json 文件
--from <path>把输入当作位于这个路径
--profile <name>覆盖 defaultProfile
--targets <list>按你要支持的最低浏览器版本审计产物,如 "safari 14, ios_saf 13"
--fail-on <list>遇到 warnings、continuity 或 compatibility 时以 1 退出;可逗号组合,或写 any
--all连未改动的声明一起列出
--css打印编译后的完整 CSS,而不是对照表
--json输出一份带版本号的 JSON 报告,供 CI、编辑器与看板集成
--color / --no-color强制开/关颜色;都不写则跟随终端,并遵守 NO_COLOR
--停止解析选项;其后所有参数均视为文件路径,包括以 - 开头的文件名
-h, --help帮助
-v, --version输出当前安装包版本,不执行编译

版本查询与帮助一样属于信息输出:即使带 --json 也只打印纯文本版本行,不加载配置或输入文件。同时指定帮助和版本时,优先输出帮助。

需要值的长选项支持两种写法:--profile app 或 --profile=app。

退出码:编译及所有已请求门禁均通过为 0;门禁失败、参数错误、文件读不到、配置非法为 1。所以可以直接串进 shell 判断。

读配置 ​

不传 -c 就用内置默认值,表头会告诉你当前是哪几个画布。

bash
npx adaptive-matrix src/app.css -c adaptive.config.mjs

配置模块要默认导出插件选项对象,不是 PostCSS 配置。两者不一样时单独写一个:

js
// adaptive.config.mjs
import { appPcPreset } from 'postcss-adaptive-matrix'
export default appPcPreset({ appDesignWidth: 375, pcDesignWidth: 1440 })

TypeScript 配置需要一个加载器:

bash
npx tsx node_modules/postcss-adaptive-matrix/dist/cli.js src/app.css -c adaptive.config.ts

(Node 22.6+ 自带类型擦除,直接 npx adaptive-matrix -c adaptive.config.ts 也能跑,但会打一条 experimental 警告,且只支持可擦除的写法。)

用 JSON 写配置 ​

路径以 .json 结尾时,按文件读取,不走 import。在 $schema 里写上已发布的 JSON Schema,编辑器就能补全选项名、显示每一项的说明,并在取值越界时当场标出来——不用等到跑起来:

json
{
  "$schema": "https://moresyl.github.io/postcss-adaptive-matrix/schema/options.json",
  "defaultProfile": "app",
  "profiles": {
    "app": { "designWidth": 375, "fluid": { "minWidth": 320, "maxWidth": 600 } },
    "pc": { "designWidth": 1440, "fluid": { "minWidth": 1024, "maxWidth": 1920 } }
  }
}

$schema 不是配置项,读取时会被丢掉。

JSON 装不下正则和函数,所以按正则路由、或按文件动态算 designWidth 的配置只能继续用 JavaScript 写。Schema 里这些位置都用 x-also 标了出来。

配置在读第一个样式文件之前就会被校验,所以 defaultProfile 写错、fluid 区间反了,报的是编译器自己的那句话,不会先刷一屏对照表再报错。

default 漏写(export const options = {...})或预设忘了调用(export default appPcPreset 而不是 appPcPreset({...}))都会直接报错。这两种写法此前会静默地用内置默认值跑完——表头照样列出画布,对照表看着也对,没有任何地方说过你的配置根本没被读到。

验证文件路由 ​

按路径判定画布是最容易配错、又最不容易发现的一项——写错了不报错,只是路由静默失效(见构建工具集成)。--from 就是用来在上线前把路由试一遍的:

bash
# 同一份 CSS,假装它在 desktop 目录下
npx adaptive-matrix scratch.css -c adaptive.config.mjs --from src/desktop/scratch.css

两次输出的数字不一样,说明路由命中了。一样,就是没命中。

Vue SFC 的路径带 query 串,照抄进去即可:

bash
npx adaptive-matrix scratch.css --from 'src/views/mobile/home/index.vue?vue&type=style&lang.scss'

断点处的倒退 ​

两张设计稿各自都对,接缝处未必对。这个检查专门找一种情况:窗口变宽,某个尺寸反而变小了。

  shrinks .card font-size gets smaller at 768px: 17.57px → 16.18px
          clamp(0.94867rem, ...) → clamp(1.01125rem, ...). Widening the window makes this smaller — the two canvases disagree here.

.card 的字号在 App 稿上写 16px、PC 稿上写 18px,两个数字单独看都合理。但 App 稿到 767px 时已经流体放大到 17.57px,而 PC 稿从 768px 起步只有 16.18px。于是把浏览器拉宽一个像素,正文字号会突然变小。

之所以值得单独检查,是因为它只在那一个宽度上出现:两张稿子各自渲染都正常,日常调试的 375 和 1440 也都正常。

检查会在已知宽度断点两侧采样可求值的长度,并比较其绝对值。这是一项诊断,不是所有宽度上连续性的证明:无法解析的表达式、不支持的条件以及依赖布局的值仍可能无法判断。

哪些值会参与检查 ​

作者原本写下的样式也可能在断点处缩小。例如,下面两侧都是纯像素值:

css
/* Quasar 2.19 自己的样式表 */
.q-tooltip { padding: 6px 10px }
@media (max-width: 599.98px) { .q-tooltip { padding: 8px 16px } }

两侧都是纯像素的声明不会进入这项诊断。工具无法仅凭这些数值确定作者的意图。

至少一侧需要包含数学函数(calc、min、max 或 clamp)或视口长度。这种语法启发式包含 strategy: 'viewport' 的裸值输出,也包含作者手写的公式和视口值,并不追踪代码来源:发现的问题可能在源样式里就已存在。将问题归因于转换之前,应先核对原始声明。两侧采样值仍须都能求值,才会报告缩小。

代入发生在判断之前,所以「声明只写了 var(--x)、公式在 token 里」也算数。

比的是绝对值而不是数值:负长度(负外边距、外溢)本来就靠远离零来变大,-16px 编译出的公式是越宽越小的。断点两侧变号则一律不报——零是编译器自己不会跨过去的界,在这里遇到它说明两张稿的意图本就不同,谁对谁错不是这个检查能判断的。

怎么改由你决定——抬高 PC 稿的字号、把 pcFluidMin 从 1024 降到 768、或者收窄 App 稿的 fluid.maxWidth。工具只负责告诉你接缝在哪。

检查刻意收得很窄,宁可不报也不误报:只比对同一个选择器串、不做优先级推算、不展开简写;遇到 @supports、@container、非纯宽度的媒体查询、跨层叠层的分组,以及 env() / % / 容器单位这类算不出数值的值,整组跳过。

主题 token 会被代入 ​

组件库的尺寸几乎不写字面量。Vant 4.10.0 的 3198 条普通声明里,有 1173 条完全经 var() 读取——检查若在第一个 var( 就放弃,看到的就只是一小片,而且恰好避开了本插件要适配的那一层。

所以同一份样式表里的自定义属性会先被代入,再求值:

css
:root,:host { --card-width: 40px }
.card { width: var(--card-width) }
@media (min-width: 768px) { .card { width: 20px } }

这里 .card 在 768px 处从 40px 掉到 20px,代入之后才看得见。

代入只在值由视口宽度单独决定时进行,两条都要满足:

  • 只声明在 :root / :host / html 上,别处没有第二份。.van-theme-dark { --card-width: ... } 一出现就整个放弃——元素的取值取决于祖先有没有那个 class,这不是宽度能回答的。
  • 每一处声明要么无条件,要么位于纯像素宽度的 @media 里。@supports、@container、方向查询里的声明同样放弃。

token 本身在断点处被改写也算一次接缝,即使消费它的规则只写了一次:

css
:root { --card-width: 40px }
@media (min-width: 768px) { :root { --card-width: 20px } }
.card { width: var(--card-width) }   /* 只有一条,照样报 */

var(--x, 16px) 的兜底值只在 --x 确实没被声明时使用——这与浏览器一致。若 --x 被声明了但取值不可知(比如上面的主题 class),则不报,而不是拿兜底值当答案。

自定义属性声明本身(--x: ... 这一条)仍然不检查。它不是屏幕上的长度,方向对不对由消费方决定——本插件自己的 --adaptive-root-width 就是反的:它喂给 max(0px, (100vw - var(--adaptive-root-width)) / 2),值变小正是为了让留白变大。跳过它不等于忽略它:消费它的那条声明会被检查,代入之后它的方向才有意义。

实测(Vant 4.10.0 完整样式表,195 KB):可求值的值分量从 622 条(17.6%)升到 1309 条(36.9%),收录 779 个 token。剩下的是关键字、颜色和百分比——本来就不是视口相关的长度。

不能把 shrinks 报告直接解释为编译器引入了回归:断点处的缩小可能已经存在于源样式表中。应对比源文件和编译产物的诊断,再检查相关规则。同样,零报告不证明零误报,也不认证布局连续性,因为无法求值的内容和条件会被跳过。请针对实际包版本重新运行组件库验证器,不要依赖历史汇总数量。

要让构建直接失败,同一个检查也从包里导出:

js
import postcss from 'postcss'
import adaptiveMatrix, { findContinuityIssues } from 'postcss-adaptive-matrix'

const result = await postcss([adaptiveMatrix(options)]).process(css, { from })
const issues = findContinuityIssues(result.root)
if (issues.length) throw new Error(`${issues.length} 处断点倒退`)

目标浏览器读不懂的语法 ​

--targets 收一串「浏览器 + 你打算支持的最低版本」,逐条对着编译产物核:

可以重复传入 --targets;各次会按浏览器合并并保留最低版本,分段生成的目标列表也不会意外放宽兼容范围。

bash
npx adaptive-matrix src/app.css -c adaptive.config.mjs --targets "ios_saf 13, chrome 90"
  需要 @layer — iOS Safari 13 < 15.4,Chrome 90 < 99
          来源:root.layer,appPcPreset 将其设置为 'adaptive-matrix' …
          发现:@layer adaptive-matrix { :where(#app) {
          不支持时:整个 @layer 块会被丢弃,连同根级基础样式一起消失 …
          替代:root.layer: false 会输出不带包裹层的相同规则 …
  需要 clamp()、min()、max() — iOS Safari 13 < 13.4-13.7

四行的顺序是有意的:先说丢什么,再说换成什么。「iOS Safari 13 太老了」单独拿出来没法行动,而 CSS 支持缺口真正要紧的一直是「跟着一起消失的有多少」——值读不懂丢一条声明,选择器读不懂丢一整条规则,@ 规则读不懂丢一整块。

上面 clamp() 那条差的是一个小版本:13.4 就有了。人工比对版本号时这种差距最容易看漏。

目标全都够用时只有一行:

  所有目标浏览器都能读取本次输出中的 7 项 CSS 特性

已知名字:chrome、edge、safari、firefox、ios_saf、samsung;android、webview 归到 chrome。名字不认识会报错并以 1 退出,不会静默跳过——被悄悄丢掉的目标比没有审计更糟,因为它读起来像通过了。

完整的特性 × 版本表、每一项的降级路径,以及编程式 API,见浏览器特性支持与降级。

CI 质量门禁 ​

发现与阻断分离:普通预览会报告问题但退出 0;只把你希望阻断流水线的类别显式打开:

bash
npx adaptive-matrix src/app.css \
  --targets "ios_saf 15.4, chrome 99" \
  --fail-on warnings,continuity,compatibility
  • warnings:编译器诊断,例如未知 @adaptive profile;
  • continuity:编译后的长度在断点处倒退;
  • compatibility:产物语法超出 --targets 声明的浏览器能力。选择它却没传 --targets 会直接报错,绝不会空跑成通过。

any 一次启用三类。完整发现仍会输出,随后命令以 1 退出,并把简洁计数写到 stderr。配合 --css 时 stdout 仍然只有 CSS;配合 --json 时编译仍为 "ok": true,独立的 gate 表示策略是否通过,调用方可以区分「输入非法」与「产物有效但违反策略」。

机器可读报告 ​

结果交给程序而不是人读时,用 --json:

bash
npx adaptive-matrix src/app.css src/admin.css \
  -c adaptive.config.json \
  --targets "ios_saf 13, chrome 90" \
  --json > adaptive-report.json

即使输入多个文件,命令也只写一份 JSON 文档。顶层结构稳定且带版本号:

json
{
  "formatVersion": 1,
  "ok": true,
  "profiles": { "default": "app", "authored": ["app", "pc"], "libraries": 6 },
  "targets": { "ios_saf": "13", "chrome": "90" },
  "gate": { "failOn": ["continuity", "compatibility"], "passed": false },
  "summary": {
    "files": 2,
    "declarations": 31,
    "converted": 18,
    "unchanged": 13,
    "warnings": 0,
    "continuityIssues": 1,
    "compatibilityFindings": 2
  },
  "files": []
}

每个文件都带声明变化(context、prop、before、after)、编译器告警、结构化断点连续性问题;传了 --targets 时,还会包含稳定特性 ID、实际语法样本、受影响浏览器、失败方式及降级方案。未传 --fail-on 时 gate 为 null。默认 changes 只含已转换/生成的声明;加 --all 才包含原样保留项。

失败同样可机器解析,并保留退出码 1:

json
{
  "formatVersion": 1,
  "ok": false,
  "error": { "message": "defaultProfile \"ghost\" does not exist." }
}

--json 与 --css 是互斥的输出协议。JSON 一律写 stdout,且不含 ANSI 颜色码。TypeScript 调用方可以直接从包中导入 CliJsonReport、CliSuccessReport、CliErrorReport、CliQualityGateReport 与 CLI_REPORT_FORMAT_VERSION,无需自行重复声明结构。

看整份产物 ​

结果okgate退出码
处理成功,未请求门禁truenull0
处理成功,所选门禁通过truepassed: true0
处理成功,所选门禁失败truepassed: false1
参数、配置、读取或编译错误false不存在1

对于成功解析且格式版本受支持的报告,只有 report.ok && report.gate?.passed !== false 才应接受;同时要求子进程正常退出且退出码为 0。这个条件不是运行时 JSON Schema 校验:无输出、非法 JSON 和进程失败仍需分别处理。

多文件 JSON 运行中,如果后续任一文件读取或编译失败,只会输出一份错误文档,不会返回部分成功报告;此前文件的结果也不会出现在这份错误响应中。相反,全部编译成功但所选质量门禁失败时,会保留完整文件列表和汇总,并返回 ok: true、gate.passed: false。自动化调用应同时检查退出码和这两类结果的区别。

要接着给别的工具处理,用 --css:

bash
npx adaptive-matrix src/app.css --css > out.css

这是 shell 重定向,不是原子写文件功能。编译开始前,shell 就可能清空已有的 out.css,因此绝不要重定向到输入文件本身。多文件 CSS 输出在后续文件失败时也可能只有部分内容;质量门禁失败仍会输出 CSS。用于构建产物时,应先把 stdout 保存到临时文件,确认退出码为 0 后再替换目标文件,失败则保留旧产物。若构建需要先检查结果再决定如何写入,可使用程序化 API。

--css 模式的 stdout 只有样式表文本;编译器警告、连续性发现和浏览器兼容证据全部走 stderr,包括门禁失败背后的详情。

逐文件 CSS 和对照输出遵守 stdout 背压:下游缓冲区满时,CLI 会等待后再处理下一个输入。等待期间流关闭或报错会让命令失败,并清理临时监听器。这不是恒定内存编译:每份样式表仍完整读取和解析,JSON 报告仍先汇总再序列化。下游失败后,已经送出的字节无法撤回。

多个输入会独立编译,按参数顺序输出,以换行分隔。这不是打包:不会解析或调整 import,不会重写相对资源 URL,也不会合并各文件生成的基础样式。不能假设拼接产物与分别加载样式表的行为相同。应让每份输入对应独立输出产物,或交给构建工具完成打包及资源解析。

读懂输出 ​

JSON 报告、帮助文本和最终批次汇总同样等待 stdout 背压。如果 JSON 报告写入失败,CLI 会向 stderr 报告输出错误,不会尝试往已损坏的流追加另一个 JSON 对象。

  • 16px → clamp(...) —— 换算了
  • 灰色无箭头的一行 —— 原样保留(--all 才显示)。细线、@font-face 里的长度、被忽略注释标记的声明都在这里
  • + ... —— 编译器新增的声明,比如根容器基础样式、preserveOriginal 的降级值
  • @media (min-width: 768px) › .page —— 声明所在位置由外向内。@adaptive pc 编译后就是这个样子,同一个 .page 出现两次是正常的
  • warning ... —— 编译器的告警,原样透传
  • shrinks ... —— 跨断点时尺寸倒退,见断点处的倒退
  • needs ... —— 目标浏览器读不懂的语法,只在传了 --targets 时出现,见目标浏览器读不懂的语法

表头的 +5 library canvases 是内置组件库各自的画布。它们由注册表生成,不是你能在 @adaptive 里写的名字,所以只报个数。完整清单见组件库适配。

基于 MIT 协议发布