Relay Compiler Playground:将 Rust Relay 编译器编译为 Wasm 构建网页版编译器实验室
【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relay
Relay Compiler Playground 是 Relay 项目中一个特殊的 Rust crate,它把 Rust 版 Relay 编译器的核心管线(语法解析、Schema 构建、IR 构建、变换、类型生成)通过wasm-bindgen编译为 WebAssembly 模块,并以浏览器网页的形式向开发者开放完整的编译过程可视化。本文将从该 crate 的定位出发,讲解如何构建 Wasm 模块、发布 NPM 包、运行测试,并结合源码分析它暴露的六类编译能力 API 与诊断返回结构,让读者既能复现构建流程,也能理解编译器各阶段的真实产物形态。
Relay Compiler Playground 是什么
在 compiler/crates/relay-compiler-playground/README.md 中,这个 crate 的定位被一句话概括:
Compile parts of the Rust Relay compiler to Wasm and expose them as a web-based playground.
也就是说,它并不直接参与 Relay 的日常编译,而是一个面向浏览器环境的编译器前端:把 Rust 编写的 Relay 编译器“部分能力”编译成 Wasm,再暴露为网页可调用的 JavaScript 接口。该模块对应的应用场景是 Relay 官网的 Compiler Explorer(编译器实验室),开发者可以在网页中同时编辑 GraphQL Schema 与查询文档,实时查看编译各阶段的输出。
从 Cargo.toml 可以看到它的依赖组成,这正是它能力的来源:
graphql-syntax:GraphQL 可执行文档的语法解析;graphql-ir:在 Schema 约束下构建 IR(Intermediate Representation);graphql-text-printer:将 IR 打印回 GraphQL 文本;relay-transforms:应用 Relay 的各类编译变换(inline、fragment spread、规范化等);relay-codegen:打印 Reader AST 与 Normalization AST;relay-typegen:生成 Flow / TypeScript 类型;relay-schema+schema:从 Schema 文本构建类型系统;relay-config:承载 ProjectConfig 与 FeatureFlags;intern:字符串驻留(string interning),编译器内部的字符串优化基础设施。
crate 的库类型被配置为crate-type = ["cdylib", "rlib"],其中cdylib正是为了输出 Wasm 动态库而设置。同时 Cargo.toml 中为 wasm-pack 配置了 release 档的优化参数:
[package.metadata."wasm-pack.profile.release"] wasm-opt = ["-Oz", "--enable-mutable-globals"]并在[profile.release]中设置了opt-level = "s",即优先压缩 Wasm 产物体积。
构建 Wasm 模块
构建前置条件是安装wasm-pack,且版本不低于 0.10.0。若尚未安装,用cargo install wasm-pack安装。此外 README 提到可能还需要添加rust-src组件:
rustup component add rust-src随后在 crate 目录下执行构建(--target web表示产物面向浏览器原生 ES Module):
cd compiler/crates/relay-compiler-playground wasm-pack build --target web构建完成后,NPM 模块会生成在pkg/目录下,即relay-compiler-playground/pkg。该目录包含编译好的.wasm文件、胶水 JavaScript 以及由 Cargo.toml 中的name、version等元数据自动生成的package.json。
关于版本号的说明:README 中明确“Bump the version incargo.toml. This will be used for the generatedpackage.json”,即发布版本的唯一来源是 Cargo.toml 的version字段,wasm-pack 会据此生成 NPM 包的版本。当前仓库中该 crate 的版本为0.0.3(见 Cargo.toml)。
构建目标与平台差异
README 在测试一节特别标注了一句:
NOTE: We build for node in tests and web to publish
也就是说,测试与发布使用不同的 Wasm target:
- 发布给浏览器使用:
wasm-pack build --target web; - 测试用(Node 环境跑 Jest):
wasm-pack build --target nodejs。
两种目标生成的胶水代码加载方式不同,需要按运行环境分别构建。
发布 NPM 包
发布流程分三步:
- 修改 Cargo.toml 中的
version字段,版本号会被 wasm-pack 同步用于生成的package.json; - 按上文执行
wasm-pack build --target web完成构建; - 进入
pkg目录并发布:
cd pkg npm publish运行与测试
单元 / 集成测试
仓库为该 crate 提供了基于 Jest 的测试封装。测试入口在tests/relay_compiler_playground-test.js,它通过 index.js 直接require('./pkg/relay_compiler_playground')加载编译产物。而 crate 根目录的 package.json 只是一个“用于测试的包装”(A wrapper around relay-compiler-playground used for testing),依赖jest@^27.0.3。
测试运行流程如下:
cd compiler/crates/relay-compiler-playground wasm-pack build --target nodejs # 测试用 Node 目标 yarn yarn test测试用例非常完整,覆盖了成功与失败两类路径:
成功路径(Ok):
parse_to_ast:解析文档并断言输出以ExecutableDocument开头;parse_to_ir:解析后断言输出以Operation开头;parse_to_reader_ast/parse_to_normalization_ast/transform:与快照对比;parse_to_types:分别传入{"language": "flow"}与{"language": "typescript"}验证两种类型生成语言;parse_to_reader_ast @required:验证@required(action: LOG)指令在 Reader AST 中正确生成RequiredField。
失败路径(Err):
- 非法 FeatureFlags JSON(如
{"this_key_does_not_exist": false})返回ConfigError,且错误信息中会列出全部合法字段名; - 语法错误的文档返回
DocumentDiagnostics,包含行、列区间与诊断消息; - 引用 Schema 中不存在的字段(如
does_not_exist)同样返回DocumentDiagnostics,消息为The typeUserhas no fielddoes_not_exist``; - Schema 本身引用未定义类型(如
InvalidType)时返回SchemaDiagnostics; parse_to_types传入非法语言(如{"language": "should_not_exist"})返回TypegenConfigError,并提示合法取值javascript、typescript、flow。
手动验证(接入 Docusaurus 网站)
README 提供了“手动测试”路径:构建 Wasm 后通过yarn link把本地包接入 Relay 官网(Docusaurus 站点),随后启动开发服务器验证。
cd compiler/crates/relay-compiler-playground/pkg yarn link cd ~/fbsource/xplat/js/RKJSModules/Libraries/Relay/oss/__github__/website yarn link relay-compiler-playground # 可能需要清除 Docusaurus 缓存 npx docusaurus clear # 以开发模式启动网站 yarn start启动后访问http://localhost:3000/compiler-explorer即可在浏览器中打开 Compiler Explorer 页面。需要说明的是,README 中的网站路径是针对 Facebook 内部代码库的绝对路径;在当前仓库中,对应的站点源码位于 website/src/pages/compiler-explorer.js,Docusaurus 相关配置见 website/docusaurus.config.js。
暴露的编译器 API:六个 Wasm 导出函数
Wasm 模块的全部能力由 src/lib.rs 中的六个#[wasm_bindgen]导出函数提供。它们全部以字符串为输入、以字符串为输出,且都遵循同一个模式:内部执行一个*_impl纯 Rust 函数,再通过serde_json::to_string序列化返回。返回值的 JSON 结构是一个结果枚举(Result<String, PlaygroundError>),即要么是{"Ok": "..."},要么是{"Err": {...}}。
| 导出函数 | 签名 | 功能 |
|---|---|---|
parse_to_ast | (document_text) -> String | 仅解析文档,输出语法树(AST)的 Debug 格式 |
parse_to_ir | (schema_text, document_text) -> String | 先构建 Schema,再构建 IR,输出 IR Debug 格式 |
parse_to_reader_ast | (feature_flags_json, schema_text, document_text) -> String | 应用全部变换后,输出 Reader AST(运行时读取用) |
parse_to_normalization_ast | (feature_flags_json, schema_text, document_text) -> String | 应用全部变换后,输出 Normalization AST(响应规范化用) |
parse_to_types | (feature_flags_json, typegen_config_json, schema_text, document_text) -> String | 生成 Flow / TypeScript 类型声明 |
transform | (feature_flags_json, schema_text, document_text) -> String | 应用全部变换后,把 IR 打印回 GraphQL 文本 |
AST 阶段:parse_to_ast
parse_to_ast_impl(src/lib.rs)的调用链最简单:直接调用graphql_syntax::parse_executable对文档做语法解析,解析结果通过format!("{:?}", document)以 Debug 形式输出。它只验证语法正确性,不涉及 Schema。测试中一个明显缺少选择集的错误文档会在此处报出Expected a selection: field, inline fragment, or fragment spread的诊断。
IR 阶段:parse_to_ir
parse_to_ir_impl(src/lib.rs)引入了 Schema:
- 先用
graphql_syntax::parse_executable解析文档; - 再调用
relay_schema::build_schema_with_extensions_parallel从 Schema 文本构建类型系统(错误会以SchemaDiagnostics返回); - 最后调用
graphql_ir::build(&schema, &document.definitions)在 Schema 约束下做语义检查并构建 IR。
因此在 IR 阶段,文档中引用了 Schema 不存在的字段就会报出语义错误(测试中的does_not_exist用例即在此被拦截)。IR 产物按定义逐条format!("{:?}", ...)输出并拼接。
变换与产物打印:reader / normalization / transform
这三个函数共享同一条底层管线。get_programs(src/lib.rs)负责完成“解析 → 构建 IR → 组装 Program → 应用全部变换”:
let document = graphql_syntax::parse_executable(document_text, Generated)?; let ir = graphql_ir::build(schema, &document.definitions)?; let program = Program::from_definitions(schema.clone(), ir); let base_fragment_names = Arc::new(Default::default()); apply_transforms( project_config, Arc::new(program), base_fragment_names, Arc::new(NoopPerfLogger), None, None, vec![], )?apply_transforms来自relay_transforms,返回的Programs结构体持有reader、normalization、typegen、operation_text等多个视图(Program 集合),分别对应运行时不同的产物需求。这也是为什么一次apply_transforms可以同时支撑三种不同的导出函数。
parse_to_reader_ast_impl:遍历programs.reader中的 fragments 与 operations,通过relay_codegen::print_fragment/print_operation打印为 JSON 化的 Reader AST。快照中可以看到 Fragment 与 Operation 交替输出,结构包括argumentDefinitions、selections、storageKey等字段;对带@required(action: LOG)的字段会生成RequiredField节点(见 测试快照)。parse_to_normalization_ast_impl:遍历programs.normalization的 operations,打印为 Normalization AST,用于 Relay 运行时对服务器响应做规范化写入(快照中每个字段都带有concreteType、storageKey、alias、args等字段)。transform_impl:遍历programs.operation_text中的 operations 与 fragments,使用graphql_text_printer::print_operation/print_fragment把变换后的 IR 打印回纯 GraphQL 文本,因此 Compiler Explorer 的 “Operation” 输出页看到的正是规范化后可直接发送的查询文本。
类型生成:parse_to_types
parse_to_types_impl(src/lib.rs)额外接收一个typegen_config_json参数,用于指定类型生成语言。流程为:
- 从
feature_flags_json构建FeatureFlags,从typegen_config_json构建TypegenConfig,二者共同组装出ProjectConfig(见get_project_config,src/lib.rs); - 构建
FragmentLocations后,对每个 fragment 调用relay_typegen::generate_fragment_type_exports_section; - 对每个 operation,先在 normalization 产物中找到对应 operation,再调用
generate_operation_type_exports_section与print_provided_variables生成类型与 variables 类型。
从 测试快照 可以看到两种语言的典型差异:Flow 输出declare export opaque type与+field: ?number语法,并import type { FragmentType } from "relay-runtime";TypeScript 输出readonly字段、FragmentRefs与" $fragmentType"等运行时类型标记。
错误与诊断的序列化格式
所有失败都以PlaygroundError枚举序列化返回,其四个变体(src/lib.rs):
DocumentDiagnostics(Vec<WasmDiagnostic>):文档相关错误,带位置信息;SchemaDiagnostics(Vec<WasmDiagnostic>):Schema 相关错误(如引用未定义类型),测试注释指出 Schema 诊断暂不包含位置信息,行列均为 0;ConfigError(String):FeatureFlags JSON 解析失败(如字段不存在);TypegenConfigError(String):Typegen 配置 JSON 解析失败(如非法语言值)。
WasmDiagnostic(src/lib.rs)由map_diagnostics从编译器的内部Diagnostic转换而来:诊断的 span 会通过TextSource::from_whole_document(...).to_span_range(...)换算成行/列区间(line_start、line_end、column_start、column_end),配合message一起输出,方便网页编辑器把错误标记渲染在对应的源代码位置。
从源码到网页:Compiler Explorer 的接入方式
Wasm 模块在 Relay 官网中的实际消费者是 website/src/pages/compiler-explorer.js。它展示了这套 API 的典型用法:
- 输入区三个标签页:Schema、Document、Config。Config 页把六个 Feature Flags 渲染为复选框,并允许在 Flow / TypeScript 之间切换类型生成语言;
- 输出区六个标签页:Operation、AST、IR、Normalization AST、Reader AST、Types,与上面六个 Wasm 导出函数一一对应;
- 状态持久化:编辑器的内容会序列化到 URL hash 与 localStorage(见 website/src/compiler-explorer/ExplorerState.js),刷新页面、分享链接即可复现相同的编译输入;
- 异步加载:Wasm 模块在
useEffect中通过require('relay-compiler-playground')延迟加载并调用init()初始化,返回 null 之前界面显示 “Loading...”——这是因为在 Docusaurus 构建期预渲染时加载 Wasm 会崩溃,必须放到浏览器运行时; - FeatureFlags 的 JSON 化:
useSerializedFeatureFlags会把 UI 上的布尔开关按enum(enabled/disabled)与bool两种形态归一化后传给 Wasm 函数,与 Rust 侧FeatureFlags的反序列化结构对齐。
默认的输入样例定义在 website/src/compiler-explorer/ExplorerStateConstants.js:一个User类型的 Schema(name、age、best_friend)加上一个包含 fragment spread 的MyQuery文档,与测试用例保持一致,打开页面即可直接体验六个输出页。
小结
Relay Compiler Playground 的价值在于把 Rust 版 Relay 编译器的内部管线完整搬到浏览器:语法(AST)、语义(IR)、变换结果(GraphQL 文本)、Reader AST、Normalization AST 与类型定义(Flow / TypeScript)全部可以实时生成与对比。对编译器开发者而言,它是一台可交互的调试台;对 Relay 使用者而言,它是理解编译器各阶段产物形态的直观窗口。整个 crate 体积小、边界清晰:六个 Wasm 导出函数 + 一个结果枚举,即可串联起graphql-syntax → graphql-ir → relay-transforms → relay-codegen / relay-typegen的完整调用链,是学习 Rust Relay 编译器模块划分与调用关系的极佳入口。
【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relay
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考