# 命令行预览

[English](./cli.md) · **简体中文**

改一个 `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](https://moresyl.github.io/postcss-adaptive-matrix/schema/options.json)，编辑器就能补全选项名、显示每一项的说明，并在取值越界时当场标出来——不用等到跑起来：

```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({...})`）都会直接报错。这两种写法此前会静默地用内置默认值跑完——表头照样列出画布，对照表看着也对，没有任何地方说过你的配置根本没被读到。

## 验证文件路由

按路径判定画布是最容易配错、又最不容易发现的一项——写错了不报错，只是路由静默失效（见[构建工具集成](./integration.zh-CN.md#2-只有文件相关配置才需要-from)）。`--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` 报告直接解释为编译器引入了回归：断点处的缩小可能已经存在于源样式表中。应对比源文件和编译产物的诊断，再检查相关规则。同样，零报告不证明零误报，也不认证布局连续性，因为无法求值的内容和条件会被跳过。请针对实际包版本重新运行[组件库验证器](./libraries.zh-CN.md#这张表是核对过的)，不要依赖历史汇总数量。

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

```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，见[浏览器特性支持与降级](./compatibility.zh-CN.md)。

## 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`，无需自行重复声明结构。

## 看整份产物

| 结果 | `ok` | `gate` | 退出码 |
| --- | --- | --- | --- |
| 处理成功，未请求门禁 | `true` | `null` | `0` |
| 处理成功，所选门禁通过 | `true` | `passed: true` | `0` |
| 处理成功，所选门禁失败 | `true` | `passed: false` | `1` |
| 参数、配置、读取或编译错误 | `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](./api.zh-CN.md)。

`--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` 里写的名字，所以只报个数。完整清单见[组件库适配](./libraries.zh-CN.md)。
