@css-blocks/cli 实战指南:CSS Blocks 命令行校验与 BEM 转换工具全解析
@css-blocks/cli 实战指南:CSS Blocks 命令行校验与 BEM 转换工具全解析
导读
@css-blocks/cli 是 CSS Blocks 项目提供的一个轻量命令行界面,用于在不引入完整构建工具链的情况下,对 block 文件进行语法校验(validate)和 BEM 样式到 block 语法的迁移转换(convert)。本文基于 packages/@css-blocks/cli/CHANGELOG.md、packages/@css-blocks/cli/README.md 与其源码、测试,系统讲解该 CLI 的安装方式、两条核心命令、全局选项、错误输出的演进历程与源码级实现原理,读完即可在命令行中完成 block 文件的校验、导入解析、预处理器接入与 BEM 迁移。注意:本工具定位为“简单交互界面”,官方明确表示不打算取代正式的构建工具集成,适合快速验证与小型迁移场景。
一、CLI 的诞生与演进脉络
@css-blocks/cli 的版本历史完整记录在其 CHANGELOG.md 中,从这些变更可以看出它从无到有、逐步成熟的过程,也是理解其能力边界的钥匙:
- 0.22.0(2019-05-02):完成
@css-blocks/cli的初始实现,并同时加入预处理器(preprocessor)支持——CLI 由此可以校验.scss等非原生 CSS 的 block 文件。 - 0.23.0(2019-05-08):修复并声明 bin 脚本,使
css-blocks成为可直接调用的命令;同年 0.23.1 补充缺失的 bin 脚本。 - 0.23.1 → 0.24.0(2019-09-16):错误输出进入“体验优化期”,引入通过 sourcemap 显示选择器错误位置、显示 block 导入引用、从仅记录起始位置改为记录完整范围(range),并在上下文中展示源文件内容。
- 1.0.0-alpha.0(2019-11-22):CLI 开始使用配置文件(基于
@css-blocks/config的搜索机制)。 - 1.0.0-alpha.5(2020-02-14):为 BEM 转换新增专门包
@css-blocks/bem-to-blocks,并修复:scope选择器与状态输出、将parseSync重命名为parseRoot。 - 1.0.0(2020-04-04):移除对 Node 6/8/11 的支持(破坏性变更),Node 10 与 12+ 成为受支持版本。
- 1.4.0(2020-09-04):正确统计 block 文件中的错误数;支持向 validate 命令传递目录并自动发现其中的 block 文件。
- 1.5.0(2020-09-16):仅版本号升级,无功能变更。
这些演进在今天的命令行帮助文本与源码中均有对应体现,下面逐一展开。
二、安装与快速上手
安装方式
作为 npm 包安装到项目依赖:
yarn add @css-blocks/cli
也可以不安装、直接通过 npx 调用(适合一次性校验或迁移):
npx @css-blocks/cli validate blocks/*.block.css
npx @css-blocks/cli convert my-bem-file.css
安装后,包会通过 bin 字段注册 css-blocks 命令(见 package.json 中的 "bin": { "css-blocks": "./bin/css-blocks" })。环境要求方面,package.json 中声明 "engines": { "node": "10.* || >= 12.*" },与 CHANGELOG 中 1.0.0 移除 Node 6/8/11 支持的破坏性变更一致——旧版 Node 用户需升级到 Node 10 或 12+ 才能使用。
命令总览
执行 css-blocks --help 可看到完整的用法说明:
css-blocks <cmd> [options] block-dir-or-file...
Commands:
css-blocks validate <blocks..> Validate block file syntax.
css-blocks convert <files..> Convert BEM syntax to block file syntax.
Options:
--version Show version number [boolean]
--preprocessors A JS file that exports an object that maps [string]
extensions to a preprocessor function for that type.
--npm Allow importing from node_modules [boolean]
--alias Define an import alias. Requires two arguments: [array]
an alias and a directory.
--help Show help [boolean]
从源码看(src/index.ts),参数解析基于 yargs:scriptName("css-blocks") 设置命令名,.strict() 开启严格模式(未知命令/选项会报错),.demandCommand(1, "No command was provided.") 强制至少提供一个命令。
三、validate:block 文件语法校验
基本用法
校验一个或多个 block 文件:
css-blocks validate blocks/*.block.css
css-blocks validate simple.block.css error.block.css
校验通过时输出绿色 ok 标记与相对当前工作目录的文件路径;存在错误时输出红色 error 标记、错误信息、出错位置(文件:行:列)与带上下文的代码片段,最后汇总错误总数并以非零退出码结束。
校验目录并自动发现 block 文件(1.4.0 新增)
自 1.4.0 起,validate 命令支持传入目录:CLI 会用 glob 以 **/*.block.* 模式递归发现目录下的所有 block 文件并逐一校验(源码见 src/index.ts 中 fse.statSync(blockFile).isDirectory() 分支)。例如:
css-blocks validate path/to/blocks-dir
对应的测试用例 "can check syntax for all block files in a directory"(见 test/cli-test.ts)会 chdir 到 fixtures 目录后校验整个 basic 目录,并断言退出码为 6(即目录下 6 个文件共发现 6 个错误)。
退出码与错误计数(1.4.0 修复)
CHANGELOG 1.4.0 修复了“Correctly count the errors in block files”。源码中 validate 方法维护 errorCount:单个 CssBlockError 计 1;MultipleCssBlockErrors 会遍历其 errors 逐个累加;CascadingError 则递归下钻到根因错误(error.cause)计数。校验结束后:
if (errorCount) {
this.println(`Found ${this.chalk.redBright(`${errorCount} error${errorCount > 1 ? "s" : ""}`)} in ${resolvedBlockFiles.size} file${resolvedBlockFiles.size > 1 ? "s" : ""}.`);
}
this.exit(errorCount);
即以错误总数作为进程退出码——测试中目录校验返回 6、单文件错误返回 1、无错误返回 0 都印证了这一点。因此 CI 中可以直接用 css-blocks validate 的退出码判断是否通过。
错误输出的演进与源码级实现
0.24.0 是错误输出体验的分水岭,以下能力全部体现在当前 handleCssBlockError / displayError / displaySnippet 的实现中(src/index.ts):
- 显示文件与错误范围:
fileForError通过error.location获取出错文件,并用errorHasRange(loc)判断是否携带完整范围信息(0.24.0“Track ranges instead of only the start position”)。 - 展示源代码上下文:
displaySnippet调用extractLinesFromSource(src/extract-lines-from-source.ts)读取源文件并截取错误行前后各 1 行的片段,错误行以红色加下划线高亮,行号前缀加粗。 - 导入引用链:
CascadingError场景下会先输出根因文件的error,再输出引用方文件的caused行(对应 0.24.0“Display block import references in error output”)。测试用例"correctly displays errors in referenced blocks."完整展示了这种两层输出:先打印被导入的error.block.css中的.foo.bar冲突,再打印transitive-error.block.css中@block error from "./error.block.css"处的caused信息。 - sourcemap 映射:
displayError中若hasMappedPosition(loc)为真,会先显示 “At compiled output of …”,再显示 “Source Mapped to …” 两段代码片段(对应 0.24.0“Display selector error locations using sourcemaps”与“Use sourcemaps for errors involving non-selector nodes”)。测试中 SCSS 校验用例的输出正是这种“编译产物定位 + 源码映射定位”的双段格式。
一个最小可复现的错误示例——将两个类选择器写在同一元素上(Two distinct classes cannot be selected on the same element,见 test/fixtures/basic/error.block.css):
.foo.bar {
color: red;
}
执行 css-blocks validate error.block.css 的输出大致为:
error test/fixtures/basic/error.block.css
Two distinct classes cannot be selected on the same element: .foo.bar
At test/fixtures/basic/error.block.css:1:5
1: .foo.bar {
2: color: red;
Found 1 error in 1 file.
四、全局选项:preprocessors、npm 与 alias
三个全局选项(global: true)对所有命令生效,定义在 src/index.ts 的 argumentParser 中。
--preprocessors:接入 SCSS 等预处理器(0.22.0 起)
css-blocks validate --preprocessors ./preprocessors.js blocks/*.block.scss
参数值是一个 JS 文件的路径,该文件导出一个以扩展名映射预处理函数的对象。以仓库测试中的 test/preprocessors.ts 为例,它导出 { scss },其中 scss 是一个异步函数,接收 (fullPath, content, configuration, sourceMap) 四个参数,内部用 node-sass 渲染 SCSS,返回 { content, sourceMap, dependencies }:
const scss: Preprocessors["scss"] = async (fullPath, content, _configuration, _sourceMap) => {
return new Promise((resolve, reject) => {
sass.render(
{
file: fullPath,
outFile: fullPath.replace("scss", "css"),
data: content,
sourceMap: true,
outputStyle: "expanded",
indentedSyntax: false,
},
(err, result) => {
if (err) {
reject(err);
} else {
resolve({
content: result.css.toString(),
sourceMap: result.map.toString(),
dependencies: result.stats.includedFiles,
});
}
},
);
});
};
预处理产物会经过 BlockFactory 解析,且 sourceMap 字段正是上一节“编译产物定位 + 源码映射定位”双段错误输出得以成立的数据基础。源码中 validate 通过 require(path.resolve(options.preprocessors)) 加载该模块(src/index.ts)。
--npm 与 --alias:解析导入(0.23.0 起)
# 允许从 node_modules 导入 block
css-blocks validate --npm npm.block.css
# 定义导入别名(需要两个参数:别名与目录)
css-blocks validate --npm --alias basic ../basic alias.block.css
--npm:布尔开关,开启后 CLI 使用NodeJsImporter(来自@css-blocks/core)解析导入。--alias:数组选项,必须成对提供“别名 + 目录”,且implies: "npm"意味着使用--alias时必须同时开启--npm。源码中按每两个参数为一组构造{alias, path}列表并传入NodeJsImporter(src/index.ts)。
npm 与 alias 均对应 CHANGELOG 0.23.0 的 “Imports via npm and aliases”。测试用例 "can import from node_modules" 与 "can import with aliases"(test/cli-test.ts)验证了这两种导入方式。
五、配置文件支持:从命令行参数到 css-blocks.config
1.0.0-alpha.0 起,CLI 使用配置文件(“CLI will use configuration files now”)。其实现是:validate 会以第一个 block 文件所在目录(无 block 文件时用当前工作目录)为起点,调用 @css-blocks/config 的 searchForConfiguration 搜索配置;CLI 命令行传入的 preprocessors 与 importer 会覆盖配置文件中的对应项(src/index.ts)。
配置搜索与加载逻辑见 packages/@css-blocks/config/src/index.ts:基于 cosmiconfig 查找 css-blocks.config 系列文件(JSON/JS 等),并在 transform 阶段把相对路径解析为绝对路径、把 preprocessors / importer 的字符串路径 require 成真实模块、支持 extends 继承其他配置。仓库测试夹具 test/fixtures/config-file/css-blocks.config.json 展示了最简配置形态:
{
"preprocessors": "../../../dist/test/preprocessors.js"
}
配合该配置,css-blocks validate simple.block.scss 无需再传 --preprocessors 即可完成 SCSS 校验(对应测试 "can check syntax for a valid block file")。这样团队可以把公共的预处理器、导入器写进配置,命令行只保留文件路径参数。
六、convert:BEM 语法到 block 语法迁移(1.0.0-alpha.5 起)
第二个核心命令用于把传统 BEM 命名的 CSS 文件转换为 CSS Blocks 的 block 文件语法:
css-blocks convert my-bem-file.css
CHANGELOG 1.0.0-alpha.5 显示,为此新建了独立包 @css-blocks/bem-to-blocks,CLI 的 convert 命令只是其薄封装:argumentParser 中 convert 命令直接调用 convertBemToBlocks(argv.files),成功退出码 0、失败退出码 1(src/index.ts)。
转换行为由 test/convert-test.ts 给出实例:给定 BEM 文件
.nav { }
.nav--open { }
.nav__item { }
.nav__item--is-selected { }
执行 convert nav.css 后会生成 nav.block.css,且产物中必然包含 :scope(block 根选择器)。BEM 的 block(.nav)、元素(.nav__item)、修饰符(.nav--open、.nav__item--is-selected)会分别映射为 block 根、子样式与状态。转换器内部的完整实现可进一步阅读 packages/@css-blocks/bem-to-blocks/src 目录下的源码与 test 用例。CLI 侧在 1.0.0-alpha.5 还修复了转换相关测试失败与“Incorrect :scope selector and state output”问题。
七、CLI 在项目中的定位与使用建议
- 定位是“轻量工具”而非构建插件:README 明确“Not meant to replace a proper build tool integration”。需要完整分析、重写与产物优化时,应使用仓库中 packages/@css-blocks/webpack、packages/@css-blocks/ember-cli、packages/@css-blocks/glimmer 等集成方案。
- 适合快速验证与脚本化:
validate支持单文件、多文件与目录发现,退出码即错误数,天然适合接入 CI 或 npm scripts 做语法门禁;convert可在一键迁移 BEM 存量样式时充当脚手架。 - 错误输出可读性强:自带文件路径、行号、上下文高亮、导入引用链与 sourcemap 双重定位,调试 block 语法问题时无需额外工具。
- 保持依赖与 Node 版本匹配:当前包要求 Node 10.* 或 >= 12.*,且 1.0.0 起不再支持 Node 6/8/11。
八、参考资源
- cli/CHANGELOG.md:完整版本演进记录(本文技术脉络的原始依据)
- cli/README.md:安装与命令帮助文本
- cli/src/index.ts:参数解析、validate、错误处理与退出码的源码实现
- cli/src/extract-lines-from-source.ts:错误上下文代码片段提取
- cli/test/cli-test.ts:validate 行为测试(含错误输出、目录发现、npm/alias、SCSS 场景)
- cli/test/convert-test.ts:BEM 转换行为测试
- cli/test/preprocessors.ts:SCSS 预处理器参考实现
- cli/test/fixtures/config-file/css-blocks.config.json:CLI 配置文件示例
- packages/@css-blocks/config/src/index.ts:配置文件搜索与加载机制
- packages/@css-blocks/bem-to-blocks/src:BEM → block 转换核心实现