- 编译器
- 编程语言
- 开发工具
【免费下载链接】rescript-compiler
ReScript is a robustly typed language that compiles to efficient and human-readable JavaScript.
导读
rewatch是 ReScript 编译器仓库中自研的增量构建工具(替代传统bsb),其 Features 规范 提供了一套**构建期可选源码(build-time optional sources)**机制:包作者可以在rescript.json中给源码目录打上feature标签,由使用者通过命令行或依赖声明决定哪些目录参与编译。本文将以该规范文档为主线,结合rewatch/src下的 Rust 实现与 rewatch/tests/features 测试套件,完整讲解 feature 的声明、选择、传递与增量清理规则,帮助你用这一机制交付带可选后端、实验模块或平台专属代码的库,而不会让消费者承担无关源码的编译成本。
Features 是什么:可选源码目录的构建期开关
Features 允许一个包声明其源码树中的可选部分,这些部分在构建时可以被包含或排除。典型场景包括:
- 发布带多个后端的库(如 native / web),消费者只编译自己需要的那个;
- 内置实验性模块,默认不参与构建;
- 存放平台专属代码,按目标环境筛选。
原文档明确强调:Features 是rewatch的扩展机制,不属于旧版bsb构建配置规范(legacy build-configuration spec)。也就是说,它是在bsb之外由 rewatch 新增的rescript.json字段,旧配置完全不受影响。这一约定在源码中同样可见:config.rs 的注释写明features是 "a new feature of rewatch, and it's not part of the rescript.json spec"。
给源码目录打标签:sources中的feature属性
在rescript.json的sources数组里,任意条目都可以加一个feature属性。该目录仅当该 feature 在当前包处于激活状态时才参与构建:
{ "name": "@example/lib", "sources": [ { "dir": "src" }, { "dir": "src-native", "feature": "native" }, { "dir": "src-experimental", "feature": "experimental" } ] }规则如下:
- 未打标签的源码目录(无
feature)永远编译,无论激活了哪些 feature; - 带标签目录的
feature会向下级联(cascade)到嵌套的subdirs:子目录若未声明自己的feature,则继承父目录的。
级联逻辑在源码中有直接实现:config.rs 的Source::set_feature只对feature为None的子树补上父级标签,且不会覆盖子目录自己的显式声明;对应的单测test_feature_cascades_to_qualified_subdirs与test_feature_cascade_does_not_overwrite_explicit_child(config.rs)分别验证了这两种行为。
声明 feature 之间的关系:顶层features映射
顶层features映射用于声明 feature 名称及可选的含义关系(implication)——即某个 feature 激活时顺带激活其他 feature:
{ "features": { "full": ["native", "experimental"] } }关键点:
- 只有当需要「一个 feature 蕴含另一个」时才必须在此声明;纯叶子 feature(无蕴含关系)可以保持未声明状态,直接作为源码目录标签使用即可;
- 上述配置下,请求
full会**传递性(transitively)**激活native与experimental; - 循环关系(如
a -> b -> a)在构建期会被拒绝并报出清晰错误。
循环检测实现在 config.rs 的expand_feature:递归展开时维护一个stack,一旦发现当前 feature 已在栈中,就返回Cycle detected in features map: ...错误并列出循环链路。测试脚本 05-features-cycle-errors.sh 构造了"a": ["b"], "b": ["a"]的配置,验证rewatch build --features a会失败且错误信息包含 "Cycle detected"。
另外,collect_declared_features(config.rs)会把features映射的键、蕴含列表中的值以及所有出现在sources标签里的 feature 名汇总成集合,用于计算「未显式限制时默认全部激活」的基线。
命令行选择:rewatch build --features
运行rewatch build或rewatch watch时,通过--features参数把编译范围限制到指定集合。不带该参数时,每个 feature 都处于激活状态,整个源码树全部编译:
rewatch build # all features active (default) rewatch build --features native # only untagged + native rewatch build --features native,full # multiple features; also expands `full`行为细节:
- CLI 标志只作用于当前包(即你正在构建的那个包),不会向下流到依赖;每个依赖的激活 feature 集合来自其消费者的声明(见下一节);
- 空值(
--features ""或--features ,)会被拒绝;省略标志才表示「全部激活」。
参数解析在 cli.rs 中实现:validate_features_string对空串报错「--features must not be empty. Omit the flag to build with all features active.」,并通过value_parser挂到#[arg(long = "features")]上;parsed()会把逗号分隔的原始值切分成 feature 名列表。测试build_features_flag_rejects_empty_string与build_features_flag_strips_whitespace(cli.rs)验证了空值拒绝与前后空白剔除。rewatch watch同样接受--features(测试watch_features_flag_is_parsed),且build的 feature 参数会透传给内部 watch 参数(features_flag_round_trips_through_build_to_watch_args)。
限制依赖的 feature:对象形式的依赖条目
当消费一个使用了 features 的其他 ReScript 包时,把dependencies或dev-dependencies中的字符串简写换成对象形式,并列出想要激活的 feature:
{ "dependencies": [ "@plain/dep", { "name": "@example/lib", "features": ["native"] } ] }规则总结:
- 简写(
"@plain/dep")——消费者想要该依赖的所有 feature。这是既有行为,未启用 features 的配置不受任何影响; - 带
features的对象——消费者把依赖限制在列出的 feature 上(以及这些 feature 通过依赖自身features映射传递性蕴含的那些)。显式空列表("features": [])表示「只要未打标签的源码目录,不要任何 feature 门控代码」; - 不带
features的对象——等价于简写,所有 feature 激活。
依赖合并语义:同一个依赖被多个消费者以不同 feature 集引用时,取各请求的并集(union)。只要任一消费者要求全部 feature,该依赖就以全部 feature 构建。Features 永远是**加性(additive)**的——启用更多 feature 不会移除模块,因此并集始终安全。
这套合并逻辑在 packages.rs 的compute_active_features中实现,其文档注释逐条对应上述规则:CLI 缺席时根包取全部声明 feature;依赖条目无features字段表示要全部;带列表则精确请求;随后把所有消费者的请求求并集,再交给resolve_active_features做传递性展开。对应单测覆盖了 CLI 缺席取全部、CLI 限制并传递展开、依赖消费方限制、显式空列表(test_compute_active_features_honours_explicit_empty_features_list,packages.rs)等场景。
与其他标志的交互:type: "dev"、--prod与clean
type: "dev"与--prod和 features 是正交的。一个源码目录可以同时声明type: "dev"和feature;它只在这两个过滤器都通过时才编译(不在--prod下,且 feature 已激活)。rewatch clean忽略--features,总是清理每一个feature 门控目录下的全部构建产物,与当前激活了哪些 feature 无关——这让clean的行为在任何 feature 组合下都可预期。
源码佐证有两处:其一,packages.rs 的compute_active_features_prod_ignores_dev_dependency_feature_requests测试验证了--prod下不会因为dev-dependencies的简写(= 全部 feature)而把依赖翻成「全部激活」,只有非 dev 边的限制生效;其二,clean.rs 注释明确clean始终作用于全部源码目录集合,以便清理掉用户本次构建未启用的 feature 产物。
增量构建如何处理 feature 变化
- 关闭某个 feature:该 feature 的源码文件从构建视野中移除。下一次
rewatch build会看到缩小后的文件集合,并通过与处理已删除源文件相同的 diff 机制清理对应的构建产物(.mjs、.cmj等)。 rewatch watch场景:rescript.json中features的变更会触发全量重建(full rebuild),重新计算激活集合,并对激活的源码目录重新注册文件监听。而 CLI 的--features标志只在 watcher 启动时求值一次,修改它必须重启 watcher。
feature 关闭后旧产物被清理的行为有端到端测试验证:04-features-toggle-cleans-artifacts.sh 先全量构建出src-experimental/Experimental.mjs,再执行rewatch build --features native,断言Experimental.mjs已被移除而Native.mjs仍然存在。默认全量构建行为则由 01-features-default-all-active.sh 验证——不带--features时三个源码目录各产出一个.mjs文件。
验证 feature 名称:宽松的叶子与严格的循环
- 未知的 feature 名(出现在 CLI 输入或源码标签中)会被当作叶子 feature 接受——它们不会匹配任何东西,除非某个源码目录恰好以该名字打标签。对应实现是
resolve_active_features对未在映射中的名字直接保留(config.rs 的注释:unknown feature names are kept in the output — treated as leaf features)。 - 顶层
features映射中的循环是硬错误(hard error),错误信息会点名循环参与者,即前述expand_feature的Cycle detected报错。
完整示例:一个带可选后端与实验模块的库
综合以上全部机制,Features.md 给出的完整配置如下:
{ "name": "@example/lib", "sources": [ { "dir": "src" }, { "dir": "src-native", "feature": "native" }, { "dir": "src-web", "feature": "web" }, { "dir": "src-experimental", "feature": "experimental" } ], "features": { "all-backends": ["native", "web"] }, "dependencies": [ "@plain/dep", { "name": "@other/heavy", "features": ["native"] } ] }对应的三种构建结果:
rewatch build(在@example/lib下)——编译所有源码目录;@other/heavy只以其nativefeature 构建(因为消费者只请求了它);@plain/dep以全部feature 构建;rewatch build --features all-backends——编译src、src-native、src-web,跳过src-experimental;rewatch build --features experimental——编译src、src-experimental,跳过两个后端目录。
仓库的测试仓库中有一个可运行的真实样例:testrepo/packages/with-features/rescript.json,其结构与本例一致(src、src-native、src-experimental三个目录,features.full蕴含native与experimental,产物后缀.mjs),配合 rewatch/tests/features 下的 6 个脚本(默认全激活、CLI 限制、传递展开、切换清理产物、循环报错、空标志拒绝),可以完整复现本文介绍的所有行为。
小结
Features 为 ReScript 包提供了一套轻量、可传递、纯构建期的可选源码开关:在sources上打标签声明可选目录,用顶层features映射表达蕴含关系,消费者则通过--features或依赖对象形式精确挑选。其核心设计——叶子 feature 无需声明、未知名字宽松接受、循环硬报错、请求取并集且严格加性、clean无视 feature 组合——保证了即使多个消费者以不同 feature 集依赖同一包,构建结果也始终安全一致。对库作者而言,这意味着实验模块、平台分支和多后端实现可以随库分发,却不会强迫每个消费者为其付出编译时间。
进一步阅读:构建配置全貌见 CompilerConfigurationSpec.md,多包仓库场景见 MonorepoSupport.md;features 的 CLI 参数与包级合并逻辑可分别深入 cli.rs 与 packages.rs。
- 编译器
- 编程语言
- 开发工具
【免费下载链接】rescript-compiler
ReScript is a robustly typed language that compiles to efficient and human-readable JavaScript.
相关推荐
Mr. Data Converter安全考量:数据验证与XSS防护最佳方案
Mr. Data Converter安全考量:数据验证与XSS防护最佳方案 Mr. Data Converter是一款强大的开源工具,能够将Excel中的CSV
编译器编程语言开发工具ReScript 构建系统 rewatch 的 Monorepo 支持:上下文推断、依赖解析与构建范围详解
ReScript 构建系统 rewatch 的 Monorepo 支持:上下文推断、依赖解析与构建范围详解 本篇技术指南围绕 rewatch(ReScript
编译器编程语言开发工具Blender源码构建系统:CMake配置与编译选项详解
Blender源码构建系统:CMake配置与编译选项详解 你是否曾因复杂的编译选项而对Blender源码望而却步?本文将系统解析Blender的CMake构建系
图形学3D渲染桌面应用音视频
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考