跳到正文

从一对 TypeScript 括号看 Prettier、Biome、dprint 和 Oxfmt

在写 Nuxt 组件的根 Block 命名规则时,我从 .nuxt/types/components.d.ts 摘了一条声明。文章里的代码围栏会被单独交给 Prettier,检查器认为这里少了一对括号。下面保留原始写法和格式化后的写法,方便对照。Nuxt 组件名的例子

- CommonEmptyState: typeof import('../../app/components/common/empty-state.vue')['default'];
+ CommonEmptyState: (typeof import('../../app/components/common/empty-state.vue'))['default'];

这两种写法在 TypeScript 里都表示取模块类型的 default 成员。但如果把这条声明分别交给 Prettier、Biome、dprint 和 Oxfmt,有的会加括号,有的会删,有的两种写法都保留。既然格式化器要决定这些细节,换一个工具到底能多控制多少?我又拿博客里的 TS 围栏和 Vue SFC 跑了一轮。

类型查询怎样进入索引访问

TypeScript 类型位置的 typeof registry['default'] 会先取 registry 的类型,再用 'default' 索引它。同一串字符出现在 JavaScript 表达式位置时,typeof 的操作数是 registry['default']。两种上下文的结合顺序不同,加括号可以把类型位置的顺序写明白。对于这里的 typeof import(...),有无括号都能表达同一个索引访问。Oxfmt 对括号规则的说明

我把有括号和无括号的输入都跑了一遍。决定输出的地方有两层,先看解析器交给打印器哪类节点,再看这个节点有没有补括号或省略括号的规则。

四个格式化工具对类型查询外侧括号的处理

Prettier 和 Oxfmt 会补上括号

Prettier 3.9.10 的 typescript 和 babel-ts 两条解析路径,都会把本例交给打印器当作 TSIndexedAccessType。它的 objectType 是 TSTypeQuery,类型查询的操作数则是 TSImportType。源码的括号判断直接匹配前两个条件。节选只保留与索引访问有关的判断。

key === "objectType" && parent.type === "TSIndexedAccessType"

Oxfmt 0.77.0 的解析器也会在 TSImportType 外创建 TSTypeQuery。它的打印器检查当前节点是否是索引对象,并在满足时加括号。['default'] 这一侧即使出现类型查询,也不走这条判断。Prettier 的括号判断 · Oxc 的类型查询解析 · Oxfmt 的括号判断

Biome 把这条声明当作导入类型打印

Biome 2.6.6 的解析器遇到 typeof import,会生成自带 typeof token 的 TsImportType,这个 token 与 import、路径等字段在同一个节点里。格式化前,重写器会拆掉本例输入里的括号。随后 TsImportType 的打印逻辑把这些字段依次输出,不会补回括号。普通的 typeof registry['default'] 则进入 TsTypeofType 的规则,我用同版本实测,它会被打成 (typeof registry)['default']。两个结果的分界就在节点类型。Biome 解析分支 · 括号重写 · 导入类型打印器 · 普通类型查询的括号规则

dprint 保留索引对象上已有的括号

dprint TypeScript 插件 0.99.1 依次生成索引对象和索引部分。对象上有括号时,它会遇到单独的 TsParenthesizedType 节点,再按父节点判断是否省略。索引访问不在这条省略规则的范围内,所以会保留。无括号的输入直接打印类型查询,生成器也没有额外包一层。索引访问的生成逻辑 · 省略类型括号的条件

这四个版本都没有给这种括号提供「始终加上」和「始终省略」的配置。几个工具都有名字带括号的选项,其中 arrowParens、arrowParentheses 和 arrowFunction.useParentheses 管的是单参数箭头函数。需要忠实引用生成声明时,Prettier 的 // prettier-ignore 可以让当前属性按输入输出,围栏内其他代码照常检查。Prettier 局部忽略 · Biome 的箭头函数选项 · dprint 的箭头函数选项 · Oxfmt 配置

用博客里的围栏和主题组件跑一遍

2026 年 11 月 9 日,我在 Windows 上从 94 篇文章提取了 455 个围栏,其中 445 个是 TS,10 个是 TSX。提取和跳过规则沿用博客检查器的口径,有 30 个标记跳过的围栏留在样本之外。Vue 则取 Sheng 主题格式化命令覆盖的全部 19 个 SFC,模板、脚本和样式一起交给工具处理。

每种工具都读取相同的输入。先用默认排版跑一轮,再尽量对齐现用配置,比较结果与仓库当前 Prettier 输出。多数文章和主题用 120 列,JavaScript 最佳实践系列用 180 列,并省略单参数箭头函数的括号。Biome 的 Vue 测试启用了完整文件支持和 HTML 格式化,dprint 用 TypeScript 0.99.1、markup_fmt 0.27.6 和 Malva 0.16.0 共同处理 SFC。版本、配置和实际输出节选见实测记录弹窗。Biome 的 Vue 支持说明 · dprint 的 markup_fmt 插件 · Malva 插件

表里的两个数字分别表示 TypeScript 围栏和 Vue 文件的差异数,TSX 计入 TypeScript 一列。比较前统一换行符,只要一个样本的输出有差异就计一次。Biome 拒绝格式化的围栏单独计入失败数。

工具 默认排版差异(TS / Vue) 对齐常用选项后(TS / Vue) 定向调整
Prettier 3.9.10 298 / 14 0 / 0 仓库现用输出作为基准
Oxfmt 0.77.0 282 / 14 1 / 0 CLI 与 Node API 在 474 个样本上输出一致
Biome 2.6.6 381 / 19 1 / 6 两轮各拒绝 17 个 TS 围栏,Vue 可进一步调到 0
dprint 0.64.3 281 / 14 51 / 17 再细调后是 23 / 6

Prettier 自己换回默认配置,也会改动 298 个围栏。默认轮更像是在比较工具预设了哪些风格。对齐引号、行宽、缩进等设置之后,才看得清哪些差异需要单独处理。

dprint 的细分选项和 Biome 的 Vue 空白策略

这两组配置都能缩小差异,但它们的影响范围不同。改 dprint 的类型分隔符会同时改动单字段和多字段类型。改 Biome 的模板空白策略则可能改变最终页面上的文字。

dprint 可以分别管导入、运算符和类型成员

dprint 默认会对导入语句和花括号内的命名导入排序。主题的 document.vue 在默认轮里换了 ../components 与 ../server/types 的先后顺序。将 module.sortImportDeclarations 和 importDeclaration.sortNamedImports 设为 maintain 就可以保留顺序。跨行表达式里的 &&、+ 可以通过 binaryExpression.operatorPosition: 'sameLine' 放回行尾。dprint 的导入与导出选项 · 运算符位置选项

有些选项会连带改变别处。dprint 默认会把 defineProps<{ foo: string }>() 打成 defineProps<{ foo: string; }>()。把 typeLiteral.separatorKind.singleLine 设成 comma,可以去掉这个单字段类型的尾分号,但多个字段之间也会改用逗号。对齐这项、运算符位置、JSX 属性引号、for...of 的换行偏好和 Malva 的属性选择器引号之后,仍有 23 个 TS 围栏和 6 个 Vue 文件的输出不同。Vue 文件的差异主要集中在类型成员的分隔符。博客里的 defineProps 示例 · dprint 类型分隔符 · Malva 引号选项

Vue 的斜杠和模板文字空格是两件事

Biome 会把原有的 <hr /> 打成 <hr>,通过 html.formatter.selfCloseVoidElements: 'always' 就能调回。空白处理要单独验。把现用 Prettier 的 ignore 空白策略直接映射到 Biome 后,归档标题原有的 共计 {{ page.totalPosts }} 篇文章 变成了 共计{{ page.totalPosts }} 篇文章。文章页 发布于 后的空格也被删掉了。我核对了 Vue 的 DOM 和 SSR 模板编译结果,删掉的空格确实进了产物。把 Biome 的 whitespaceSensitivity 改成 css,同时保留自闭合斜杠,19 个 SFC 的文本输出都回到了基准。Biome 的 HTML 选项

哪些差异需要调整检查方式

Biome 拒绝的 17 个围栏都是从函数或循环里截取的代码,单独提取以后,return 或 continue 缺少外层语句。开启 formatter.formatWithErrors 后可以得到输出,但命令仍报告解析错误并以 1 退出。博客把围栏当作摘录格式化,所以如果直接替换 CLI,这些例子会让检查失败。另有一个合法示例 [/* 省略菜单项 */] 被展开为多行,expand 的三个值都给出相同结果。Biome 的错误容忍选项 · 循环体摘录

Oxfmt 对齐后只剩一个不同的围栏,new Set(/* ... */) 里的注释被移到了参数列表外,变成 new Set /* ... */()。该版本的公开配置里没有控制这一位置的选项。它也保留了 Nuxt 声明现有的 // prettier-ignore。Vue 输出接近还有实现上的原因,Oxfmt 用内置的 Prettier 处理 Vue 文件,嵌入的 JS 和 TS 则交给自己的格式化器。Oxfmt 配置 · Oxfmt 的语言支持

这个博客继续用 Prettier

Prettier 把自己称为 opinionated 的格式化工具。它把类型查询的括号作为打印器的决定,项目只需要在引用 Nuxt 生成声明的地方加一条局部忽略。这次横评没有找到专管这对括号的开关,博客继续用现有的 Prettier 检查。Prettier 的配置理念

如果将来需要尽量少改已有输出,Oxfmt 是这批样本里最接近的候选。如果希望把导入顺序、跨行运算符或类型分隔符交给项目决定,dprint 的选项更细。选型时,我会先拿已有文档摘录和实际组件跑一轮,重点看默认差异能消掉多少、剩下的差异会不会影响编译或阻断检查。

格式化横评实测记录

打开原文件 formatting-comparison.json

{
  "measuredOn": "2026-11-09",
  "runtime": {
    "platform": "Windows x64",
    "node": "26.12.0"
  },
  "versions": {
    "prettier": "3.9.10",
    "@biomejs/biome": "2.6.6",
    "dprint": "0.64.3",
    "oxfmt": "0.77.0",
    "@dprint/typescript": "0.99.1",
    "dprint-plugin-markup": "0.27.6",
    "dprint-plugin-malva": "0.16.0"
  },
  "scope": {
    "markdownScanned": 301,
    "postsWithIncludedFences": 94,
    "ts": 445,
    "tsx": 10,
    "ignoredFences": 30,
    "sharedConfigFences": 8,
    "vue": 19
  },
  "method": [
    "从博客正文提取 TypeScript 和 TSX 围栏,跳过已登记的例外。以独立 .ts 或 .tsx 文件运行格式化,不补外围语句。",
    "把 Sheng 主题格式化入口覆盖的全部 Vue SFC 交给各工具处理。Biome 默认轮也开启完整 HTML 支持与格式化,dprint 各轮都加载三个插件。",
    "所有工具读取同一份源文本,以仓库当前的 Prettier 输出为基准,换行符统一为 LF。没有输出的样本只计入失败数。",
    "使用 vue/compiler-sfc 对改动前后分别执行 DOM 和 SSR 模板编译,再比较规范化后的生成代码。未测性能。",
    "dprint 的插件路径保留 npm 包名和包内文件名,实际运行时从隔离安装目录加载 WASM。"
  ],
  "baseline": {
    "normal": {
      "printWidth": 120,
      "tabWidth": 2,
      "singleQuote": true,
      "endOfLine": "lf",
      "htmlWhitespaceSensitivity": "ignore",
      "embeddedLanguageFormatting": "auto"
    },
    "shared": {
      "printWidth": 180,
      "tabWidth": 2,
      "singleQuote": true,
      "endOfLine": "lf",
      "htmlWhitespaceSensitivity": "ignore",
      "embeddedLanguageFormatting": "auto",
      "semi": true,
      "arrowParens": "avoid"
    }
  },
  "comparableConfigs": {
    "normal": {
      "biome": {
        "html": {
          "experimentalFullSupportEnabled": true,
          "formatter": {
            "enabled": true,
            "whitespaceSensitivity": "ignore",
            "indentScriptAndStyle": false
          }
        },
        "formatter": {
          "indentStyle": "space",
          "indentWidth": 2,
          "lineWidth": 120,
          "lineEnding": "lf",
          "formatWithErrors": false
        },
        "javascript": {
          "formatter": {
            "quoteStyle": "single",
            "semicolons": "always",
            "trailingCommas": "all",
            "arrowParentheses": "always",
            "quoteProperties": "asNeeded"
          }
        }
      },
      "dprint": {
        "plugins": [
          "@dprint/typescript/plugin.wasm",
          "dprint-plugin-markup/plugin.wasm",
          "dprint-plugin-malva/plugin.wasm"
        ],
        "incremental": false,
        "lineWidth": 120,
        "indentWidth": 2,
        "useTabs": false,
        "newLineKind": "lf",
        "typescript": {
          "quoteStyle": "preferSingle",
          "semicolons": "always",
          "trailingCommas": "onlyMultiLine",
          "arrowFunction.useParentheses": "force",
          "quoteProps": "asNeeded",
          "module.sortImportDeclarations": "maintain",
          "module.sortExportDeclarations": "maintain",
          "importDeclaration.sortNamedImports": "maintain",
          "exportDeclaration.sortNamedExports": "maintain"
        },
        "markup": {
          "printWidth": 120,
          "whitespaceSensitivity": "ignore",
          "vue.scriptIndent": false,
          "vue.styleIndent": false
        }
      },
      "oxfmt": {
        "printWidth": 120,
        "tabWidth": 2,
        "singleQuote": true,
        "endOfLine": "lf",
        "htmlWhitespaceSensitivity": "ignore",
        "embeddedLanguageFormatting": "auto",
        "arrowParens": "always"
      }
    },
    "shared": {
      "biome": {
        "html": {
          "experimentalFullSupportEnabled": true,
          "formatter": {
            "enabled": true,
            "whitespaceSensitivity": "ignore",
            "indentScriptAndStyle": false
          }
        },
        "formatter": {
          "indentStyle": "space",
          "indentWidth": 2,
          "lineWidth": 180,
          "lineEnding": "lf",
          "formatWithErrors": false
        },
        "javascript": {
          "formatter": {
            "quoteStyle": "single",
            "semicolons": "always",
            "trailingCommas": "all",
            "arrowParentheses": "asNeeded",
            "quoteProperties": "asNeeded"
          }
        }
      },
      "dprint": {
        "plugins": [
          "@dprint/typescript/plugin.wasm",
          "dprint-plugin-markup/plugin.wasm",
          "dprint-plugin-malva/plugin.wasm"
        ],
        "incremental": false,
        "lineWidth": 180,
        "indentWidth": 2,
        "useTabs": false,
        "newLineKind": "lf",
        "typescript": {
          "quoteStyle": "preferSingle",
          "semicolons": "always",
          "trailingCommas": "onlyMultiLine",
          "arrowFunction.useParentheses": "preferNone",
          "quoteProps": "asNeeded",
          "module.sortImportDeclarations": "maintain",
          "module.sortExportDeclarations": "maintain",
          "importDeclaration.sortNamedImports": "maintain",
          "exportDeclaration.sortNamedExports": "maintain"
        },
        "markup": {
          "printWidth": 180,
          "whitespaceSensitivity": "ignore",
          "vue.scriptIndent": false,
          "vue.styleIndent": false
        }
      },
      "oxfmt": {
        "printWidth": 180,
        "tabWidth": 2,
        "singleQuote": true,
        "endOfLine": "lf",
        "htmlWhitespaceSensitivity": "ignore",
        "embeddedLanguageFormatting": "auto",
        "arrowParens": "avoid"
      }
    }
  },
  "measurements": {
    "prettierDefault": {
      "tsDifferences": 298,
      "vueDifferences": 14
    },
    "prettierRepo": {
      "tsDifferences": 0,
      "vueDifferences": 0
    },
    "biome/default": {
      "tsDifferences": 381,
      "vueDifferences": 19,
      "tsRejected": 17,
      "tsWithParseDiagnostics": 17,
      "exitCodes": {
        "normal": {
          "first": 1,
          "second": 1
        },
        "shared": {
          "first": 0,
          "second": 0
        }
      },
      "vueTemplateDifferences": [
        {
          "file": "themes/sheng/layout/document.vue",
          "domDiff": true,
          "ssrDiff": true
        },
        {
          "file": "themes/sheng/components/search-panel/index.vue",
          "domDiff": true,
          "ssrDiff": true
        }
      ]
    },
    "biome/aligned": {
      "tsDifferences": 1,
      "vueDifferences": 6,
      "tsRejected": 17,
      "tsWithParseDiagnostics": 17,
      "exitCodes": {
        "normal": {
          "first": 1,
          "second": 1
        },
        "shared": {
          "first": 0,
          "second": 0
        }
      },
      "vueTemplateDifferences": [
        {
          "file": "themes/sheng/layout/archive.vue",
          "domDiff": true,
          "ssrDiff": true
        },
        {
          "file": "themes/sheng/layout/post.vue",
          "domDiff": true,
          "ssrDiff": true
        }
      ]
    },
    "biome/tolerant": {
      "tsDifferences": 1,
      "vueDifferences": 6,
      "tsRejected": 0,
      "tsWithParseDiagnostics": 17,
      "exitCodes": {
        "normal": {
          "first": 1,
          "second": 1
        },
        "shared": {
          "first": 0,
          "second": 0
        }
      },
      "vueTemplateDifferences": [
        {
          "file": "themes/sheng/layout/archive.vue",
          "domDiff": true,
          "ssrDiff": true
        },
        {
          "file": "themes/sheng/layout/post.vue",
          "domDiff": true,
          "ssrDiff": true
        }
      ]
    },
    "dprint/default": {
      "tsDifferences": 281,
      "vueDifferences": 14,
      "tsRejected": 0,
      "tsWithParseDiagnostics": 0,
      "exitCodes": {
        "normal": {
          "first": 0,
          "second": 0
        },
        "shared": {
          "first": 0,
          "second": 0
        }
      },
      "vueTemplateDifferences": [
        {
          "file": "themes/sheng/layout/document.vue",
          "domDiff": true,
          "ssrDiff": true
        },
        {
          "file": "themes/sheng/components/search-panel/index.vue",
          "domDiff": true,
          "ssrDiff": true
        }
      ]
    },
    "dprint/aligned": {
      "tsDifferences": 51,
      "vueDifferences": 17,
      "tsRejected": 0,
      "tsWithParseDiagnostics": 0,
      "exitCodes": {
        "normal": {
          "first": 0,
          "second": 0
        },
        "shared": {
          "first": 0,
          "second": 0
        }
      },
      "vueTemplateDifferences": []
    },
    "oxfmt/default": {
      "tsDifferences": 282,
      "vueDifferences": 14,
      "tsRejected": 0,
      "tsWithParseDiagnostics": 0,
      "exitCodes": {
        "normal": {
          "first": 0,
          "second": 0
        },
        "shared": {
          "first": 0,
          "second": 0
        }
      },
      "vueTemplateDifferences": [
        {
          "file": "themes/sheng/layout/document.vue",
          "domDiff": true,
          "ssrDiff": true
        },
        {
          "file": "themes/sheng/components/search-panel/index.vue",
          "domDiff": true,
          "ssrDiff": true
        }
      ]
    },
    "oxfmt/aligned": {
      "tsDifferences": 1,
      "vueDifferences": 0,
      "tsRejected": 0,
      "tsWithParseDiagnostics": 0,
      "exitCodes": {
        "normal": {
          "first": 0,
          "second": 0
        },
        "shared": {
          "first": 0,
          "second": 0
        }
      },
      "vueTemplateDifferences": []
    }
  },
  "targetedRuns": [
    {
      "tool": "biome",
      "mode": "vue-ignore",
      "group": "normal",
      "exit": 0,
      "secondExit": 0,
      "unstable": 0,
      "tsDiff": 0,
      "vueDiff": 2,
      "semanticDiff": [
        {
          "file": "themes/sheng/layout/archive.vue",
          "errors": [],
          "domDiff": true,
          "ssrDiff": true
        },
        {
          "file": "themes/sheng/layout/post.vue",
          "errors": [],
          "domDiff": true,
          "ssrDiff": true
        }
      ]
    },
    {
      "tool": "biome",
      "mode": "vue-css",
      "group": "normal",
      "exit": 0,
      "secondExit": 0,
      "unstable": 0,
      "tsDiff": 0,
      "vueDiff": 0,
      "semanticDiff": []
    },
    {
      "tool": "biome",
      "mode": "vue-strict",
      "group": "normal",
      "exit": 0,
      "secondExit": 0,
      "unstable": 0,
      "tsDiff": 0,
      "vueDiff": 1,
      "semanticDiff": []
    },
    {
      "tool": "biome",
      "mode": "expand-never",
      "group": "normal",
      "exit": 0,
      "secondExit": 0,
      "unstable": 0,
      "tsDiff": 1,
      "vueDiff": 0,
      "semanticDiff": []
    },
    {
      "tool": "biome",
      "mode": "expand-always",
      "group": "normal",
      "exit": 0,
      "secondExit": 0,
      "unstable": 0,
      "tsDiff": 1,
      "vueDiff": 0,
      "semanticDiff": []
    },
    {
      "tool": "dprint",
      "mode": "tuned-final",
      "group": "normal",
      "exit": 0,
      "secondExit": 0,
      "unstable": 0,
      "tsDiff": 23,
      "vueDiff": 6,
      "semanticDiff": []
    },
    {
      "tool": "dprint",
      "mode": "tuned-final",
      "group": "shared",
      "exit": 0,
      "secondExit": 0,
      "unstable": 0,
      "tsDiff": 0,
      "vueDiff": 0,
      "semanticDiff": []
    }
  ],
  "targetedSettings": {
    "biome/vue-ignore": {
      "html.formatter.selfCloseVoidElements": "always",
      "html.formatter.whitespaceSensitivity": "ignore"
    },
    "biome/vue-css": {
      "html.formatter.selfCloseVoidElements": "always",
      "html.formatter.whitespaceSensitivity": "css"
    },
    "biome/vue-strict": {
      "html.formatter.selfCloseVoidElements": "always",
      "html.formatter.whitespaceSensitivity": "strict"
    },
    "biome/expand-never": {
      "javascript.formatter.expand": "never"
    },
    "biome/expand-always": {
      "javascript.formatter.expand": "always"
    },
    "dprint/tuned-final": {
      "typescript": {
        "binaryExpression.operatorPosition": "sameLine",
        "jsx.quoteStyle": "preferDouble",
        "forOfStatement.preferHanging": true,
        "typeLiteral.separatorKind.singleLine": "comma"
      },
      "malva": {
        "attrSelector.quotes": "preferSingle"
      }
    }
  },
  "biomeRejected": [
    {
      "file": "source/_posts/2026-01-29 - Node 文件遍历工具的迭代:从 walk 到 readdir,再到 tinyglobby.md",
      "line": 85,
      "language": "ts"
    },
    {
      "file": "source/_posts/2026-05-26 - Vue script setup 中组件静默消失的原因:一次命名冲突排查/2026-05-26 - Vue script setup 中组件静默消失的原因:一次命名冲突排查(1):从静默消失定位到命名冲突.md",
      "line": 686,
      "language": "ts"
    },
    {
      "file": "source/_posts/2026-05-26 - Vue script setup 中组件静默消失的原因:一次命名冲突排查/2026-05-26 - Vue script setup 中组件静默消失的原因:一次命名冲突排查(2):顶层绑定怎样参与模板名字解析.md",
      "line": 152,
      "language": "ts"
    },
    {
      "file": "source/_posts/2026-06-22 - Teleport 弹窗尺寸为什么会丢/2026-06-22 - Teleport 弹窗尺寸为什么会丢(1):Teleport 怎样管理两个 DOM 位置.md",
      "line": 207,
      "language": "ts"
    },
    {
      "file": "source/_posts/2026-06-29 - Header 不要被页面反向控制:一次 Nuxt SSR hydration 错位复盘.md",
      "line": 176,
      "language": "ts"
    },
    {
      "file": "source/_posts/2026-07-03 - 别再用 length 统计用户文本:一次 Unicode 字符数和 ESLint 护栏复盘/2026-07-03 - 别再用 length 统计用户文本:一次 Unicode 字符数和 ESLint 护栏复盘(3):用 ESLint 发现用户文本的危险操作.md",
      "line": 19,
      "language": "ts"
    },
    {
      "file": "source/_posts/2026-07-08 - 浏览器为什么播不了 App 语音:一次 AMR 互通和策略模式改造.md",
      "line": 93,
      "language": "ts"
    },
    {
      "file": "source/_posts/2026-07-09 - 别让一个 composable 承担两种身份:一次 Profile 全局状态重构.md",
      "line": 68,
      "language": "ts"
    },
    {
      "file": "source/_posts/2026-07-09 - 别让一个 composable 承担两种身份:一次 Profile 全局状态重构.md",
      "line": 74,
      "language": "ts"
    },
    {
      "file": "source/_posts/2026-07-09 - 别让一个 composable 承担两种身份:一次 Profile 全局状态重构.md",
      "line": 95,
      "language": "ts"
    },
    {
      "file": "source/_posts/2026-08-14 - Vue KeepAlive 内部滚动恢复怎么封:库、生命周期和跨版本适配.md",
      "line": 250,
      "language": "ts"
    },
    {
      "file": "source/_posts/2026-10-19 - Happy DOM 和 jsdom:差异、选型与测试迁移/2026-10-19 - Happy DOM 和 jsdom:差异、选型与测试迁移(1):能力差异与测试环境选型.md",
      "line": 93,
      "language": "ts"
    },
    {
      "file": "source/_posts/2026-10-25 - 我所理解的 UnoCSS 最佳实践/2026-10-25 - 我所理解的 UnoCSS 最佳实践(4):Tailwind CSS 与 UnoCSS 的兼容边界.md",
      "line": 275,
      "language": "ts"
    },
    {
      "file": "source/_posts/2026-10-28 - Happy DOM 为什么会误判复杂的 not 选择器/2026-10-28 - Happy DOM 为什么会误判复杂的 not 选择器(2):选择器的解析、匹配与缓存.md",
      "line": 137,
      "language": "ts"
    },
    {
      "file": "source/_posts/2026-10-28 - Happy DOM 为什么会误判复杂的 not 选择器/2026-10-28 - Happy DOM 为什么会误判复杂的 not 选择器(4):not() 的复现与故障定位.md",
      "line": 116,
      "language": "ts"
    },
    {
      "file": "source/_posts/2026-10-28 - Happy DOM 为什么会误判复杂的 not 选择器/2026-10-28 - Happy DOM 为什么会误判复杂的 not 选择器(4):not() 的复现与故障定位.md",
      "line": 148,
      "language": "ts"
    },
    {
      "file": "source/_posts/2026-11-03 - 我所理解的 TypeScript 最佳实践/2026-11-03 - 我所理解的 TypeScript 最佳实践(2):严格模式.md",
      "line": 383,
      "language": "ts"
    }
  ],
  "examples": {
    "biomeFragment": {
      "file": "source/_posts/2026-01-29 - Node 文件遍历工具的迭代:从 walk 到 readdir,再到 tinyglobby.md",
      "line": 85,
      "language": "ts",
      "outputs": {
        "baseline": "if (!entry.isFile()) {\n  continue;\n}",
        "formatWithErrors": "if (!entry.isFile()) {\n  continue;\n}"
      }
    },
    "biomeCommentArray": {
      "file": "source/_posts/2026-07-08 - 不要用屏幕宽度判断触摸交互:一次照片网格拖拽排序修复.md",
      "line": 84,
      "language": "ts",
      "outputs": {
        "baseline": "const popoverActions = [/* 省略菜单项 */];\nconst actionSheetActions = [/* 省略菜单项 */];",
        "auto": "const popoverActions = [\n  /* 省略菜单项 */\n];\nconst actionSheetActions = [\n  /* 省略菜单项 */\n];",
        "never": "const popoverActions = [\n  /* 省略菜单项 */\n];\nconst actionSheetActions = [\n  /* 省略菜单项 */\n];",
        "always": "const popoverActions = [\n  /* 省略菜单项 */\n];\nconst actionSheetActions = [\n  /* 省略菜单项 */\n];"
      }
    },
    "biomeVueWhitespace": {
      "file": "themes/sheng/layout/archive.vue",
      "line": 1,
      "language": "vue",
      "outputs": {
        "baseline": "<h2 class=\"archive-page__count\">共计 {{ page.totalPosts }} 篇文章</h2>",
        "aligned": "<h2 class=\"archive-page__count\">共计{{ page.totalPosts }} 篇文章</h2>",
        "css": "<h2 class=\"archive-page__count\">共计 {{ page.totalPosts }} 篇文章</h2>"
      }
    },
    "dprintTypeMembers": {
      "file": "source/_posts/2026-05-23 - Vue script setup 中 defineProps 的编译边界与错误排查.md",
      "line": 10,
      "language": "ts",
      "outputs": {
        "baseline": "const props = defineProps<{ foo: string }>();",
        "aligned": "const props = defineProps<{ foo: string; }>();",
        "tuned": "const props = defineProps<{ foo: string }>();"
      }
    },
    "oxfmtNewArgumentsComment": {
      "file": "source/_posts/2026-05-26 - Vue script setup 中组件静默消失的原因:一次命名冲突排查/2026-05-26 - Vue script setup 中组件静默消失的原因:一次命名冲突排查(1):从静默消失定位到命名冲突.md",
      "line": 686,
      "language": "ts",
      "outputs": {
        "baseline": "directives: new Set(/* ... */),",
        "aligned": "directives: new Set /* ... */(),"
      }
    }
  }
}