- 编译器
- 编程语言
- 开发工具
【免费下载链接】rescript-compiler
ReScript is a robustly typed language that compiles to efficient and human-readable JavaScript.
导读
packages/playground是 ReScript 编译器中用于构建“Playground 浏览器端编译 Bundle”的核心工程:它把编译器本体(compiler.js)与一批第三方库(Belt、React 等)的编译产物打包成cmij.js文件,使得在前端环境中也能完成带完整依赖解析的 ReScript 编译。读完本文,你将掌握如何生成这些cmij.js产物、如何在浏览器 / Node 环境中加载它们并调用rescript_compilerAPI 编译代码,以及底层jsoo_playground_main.ml暴露的配置接口与返回结构。
什么是 Playground Bundle:cmij.js 的使命
ReScript 编译器本身由 OCaml 实现,通过js_of_ocaml可将其编译为可在浏览器运行的 JavaScript。但“能跑编译器”不等于“能编译用户代码”——真正编译一段let a = <div/>还需要 Belt、React 等库的接口信息(.cmi/.cmj编译产物)注入编译器的状态。
packages/playground工程的核心任务,就是把这一批依赖的编译产物以cmij.js的形式固化下来。cmij.js是由js_of_ocaml build-fs生成的虚拟文件系统镜像,代表一组可被 Playground Bundle 使用的“序列化依赖”。每个库(包括编译器内建模块)对应一个独立文件:
packages/compiler-builtins/cmij.js:编译器内建运行时模块;packages/@rescript/belt/cmij.js:Belt 标准库;packages/@rescript/react/cmij.js:React 及其相关绑定。
加载顺序上,先加载compiler.js(编译器本体),再依次加载各个cmij.js,依赖即注入编译器的状态中,之后即可编译引用这些库的代码。
环境与版本约定(Setup)
构建前需要确认两个配置文件中的版本一致性:
- packages/playground/rescript.json:声明了工程自身名称(
playground)、源码目录(src,subdirs: true)、包产物格式(esmodule,后缀.res.js)以及构建时纳入的依赖列表:
{ "name": "playground", "sources": { "dir": "src", "subdirs": true }, "package-specs": { "module": "esmodule", "in-source": false, "suffix": ".res.js" }, "dependencies": ["@rescript/belt", "@rescript/react"] }- packages/playground/package.json:
dependencies中的@rescript/belt、@rescript/react、rescript均使用workspace:^引用,即与本仓库当前源码版本保持一致;devDependencies包含rollup(^4.32.0)、@rollup/plugin-node-resolve与h3(2.0.1-rc.26)。
关键前提:generate_cmijs.mjs的注释明确要求“用与编译器 Bundle 相同的 rescript 版本构建 cmij 文件”("You need to build cmij files with the same rescript version as the compiler bundle")。版本不匹配会导致cmij.js中的接口信息与compiler.js无法兼容,因此必须基于同一份仓库源码同步构建,而不能混用不同版本的产物。
构建流程(Building)
在仓库根目录执行:
yarn workspace playground build该命令实际对应 packages/playground/package.json 中的脚本:
"build": "rescript clean && rescript build && node scripts/generate_cmijs.mjs && rollup -c && touch .buildstamp"整个流水线分为四步:
rescript clean && rescript build:清理并编译 playground 自身源码;node scripts/generate_cmijs.mjs:调用js_of_ocaml build-fs为编译器内建模块与所有依赖生成cmij.js(详见下文);rollup -c:依据 packages/playground/rollup.config.mjs 将packages/@rescript/runtime/lib/es6/*.mjs打包为compiler-builtins/stdlib下的 ESM 输出,并最终产出 UMD 格式的compiler.js;touch .buildstamp:标记一次完整构建完成。
注意:README 中“All the cmij files will now be available in the
packages/directory”里的packages/指的是packages/playground/packages/(见 packages/playground/scripts/common.mjs 中playgroundPackagesDir = path.join(playgroundDir, "packages")),与仓库根目录下存放源码包的packages/不是同一个目录。
产物目录结构
构建完成后,packages/playground/packages/下会形成如下结构(README 原文示例):
tree packages packages └── @rescript ├── belt │ └── cmij.js └── react ├── React.js ├── ReactDOM.js ├── ReactDOMRe.js ├── ReactDOMServer.js ├── ReactDOMStyle.js ├── ReactEvent.js ├── ReactTestUtils.js ├── ReasonReact.js ├── RescriptReactErrorBoundary.js ├── RescriptReactRouter.js └── cmij.js 3 directories, 12 files此外,compiler-builtins/目录存放内建模块的cmij.js,其中stdlib/子目录由 rollup 生成(对应compiler.js所需的 ES6 标准库模块)。React 相关库除cmij.js外还会展开各模块的独立 JS 文件,供浏览器运行时按需加载。
在 Playground Bundle 中加载 cmij 依赖并使用
假设compiler.js就是你的 Playground Bundle(UMD 模块,可通过module.exports或globalThis访问),典型用法如下(README 核心示例,可由 packages/playground/playground_test.cjs 验证):
const { rescript_compiler } = require("./compiler.js"); require("./packages/compiler-builtins/cmij.js"); require("./packages/@rescript/belt/cmij.js"); require("./packages/@rescript/react/cmij.js"); let comp = rescript_compiler.make(); comp.rescript.compile("let a = <div/>");由于 Belt 与 React 的接口信息已通过cmij.js注入编译器状态,上述脚本可以成功编译引用了这两个库的代码,例如:
const result = comp.rescript.compile(` @@jsxConfig({ version: 4, mode: "automatic" }) module A = { @react.component let make = (~a) => { let state = React.useState(() => 0) <div> {React.string(a)} </div> } } let sum = [1,2,3] ->Belt.Array.map(x => x * 2) ->Array.reduce(0, (acc, item) => acc + item) let a = <A a="hello" /> `);这里的关键点是:必须先require各cmij.js,再调用rescript_compiler.make()创建编译器实例。cmij.js在被加载时会把对应的编译产物注册进编译器的全局状态,之后创建出的实例才能解析 Belt / React 等模块名。
深入:generate_cmijs.mjs 的生成原理
packages/playground/scripts/generate_cmijs.mjs 是整个流程的核心,其逻辑可以拆解为三个部分:
- 重新构建依赖:脚本开头执行
yarn rescript clean与yarn rescript build,确保所有依赖的.cmi/.cmj都是与当前 Bundle 相同版本刚生成的产物。 - 内建模块单独打包:将
packages/@rescript/runtime/lib/ocaml目录单独打成compiler-builtins的cmij.js。源码注释解释了原因:内建模块必须独立成包,否则在 Playground 内编译时无法被正常使用("We need to build the compiler's builtin modules as a separate cmij. Otherwise we can't use them for compilation within the playground")。 - 按依赖列表循环打包:读取 playground 的
rescript.json中dependencies字段(@rescript/belt、@rescript/react),对每个包执行buildCmij:
function buildCmij(pkgDir, pkgName) { const libOcamlFolder = path.join(pkgDir, "lib", "ocaml"); const outputFolder = path.join(playgroundPackagesDir, pkgName); fs.mkdirSync(outputFolder, { recursive: true }); const cmijFile = path.join(outputFolder, "cmij.js"); const inputFiles = fs.readdirSync(libOcamlFolder).filter(isCmij).join(" "); exec(`js_of_ocaml build-fs -o ${cmijFile} -I ${libOcamlFolder} ${inputFiles}`); } function isCmij(basename) { return /\.cm(i|j)$/.test(basename); }isCmij只筛选.cmi(接口)与.cmj(编译单元)两类文件,js_of_ocaml build-fs会把它们连同目录结构一并序列化进单个cmij.js。因此,每新增一个需要注入 Playground 的第三方库,只需在packages/playground/rescript.json的dependencies中加入该包名,yarn workspace playground build便会自动为其生成对应的cmij.js。
底层 API:jsoo_playground_main.ml 暴露的编译器接口
Playground 所使用的浏览器端编译 API 由 OCaml 源码 compiler/jsoo/jsoo_playground_main.ml 定义,其构建规则见 compiler/jsoo/dune(executables名为jsoo_playground_main,modes js wasm,仅在%{profile} = browser时启用)。该文件通过Js.export "rescript_compiler"导出全局对象,包含:
api_version:当前为"7",用于告知前端该 Bundle 支持的功能集(注释中记录了演进历史:v3 默认 Uncurried 并引入config.uncurried,v4 加入open_modules,v5 移除.ml支持,v6 加入experimental_features与jsx_preserve_mode,v7 加入调试输出 API)。version:编译器版本号(对应Bs_version.version)。make():创建编译器实例,返回带以下方法的对象:
| 方法 | 说明 |
|---|---|
rescript.compile(code) | 编译一段 ReScript 源码,返回{ js_code, warnings, type_hints, type } |
rescript.compileWithDebug(code) | 编译并额外输出parsetree、typedtree、lambda、lam等调试产物 |
rescript.format(code) | 对源码做格式化(ReScript → ReScript 美化打印) |
rescript.version | 该语言后端的版本 |
convertSyntax(fromLang, toLang, src) | 语言间语法转换(当前仅支持res) |
setModuleSystem(value) | 设置"commonjs"或"esmodule" |
setFilename(value) | 设置源码文件名(默认playground.res) |
setWarnFlags(value) | 设置警告 flag(默认Bsc_warnings.defaults_w) |
setOpenModules(list) | 隐式打开模块列表(对应config.open_modules) |
setExperimentalFeatures(list) | 启用实验性特性(如"LetUnwrap") |
setJsxPreserveMode(bool) | 是否开启 JSX preserve 模式 |
getConfig() | 读取当前配置(module_system、warn_flags、jsx_preserve_mode、experimental_features、open_modules) |
编译结果中type字段的可能取值,从Error_ret模块可以完整梳理:"success"、"syntax_error"、"type_error"、"warning_error"、"warning_flag_error"、"unexpected_error"。其中warnings数组中的每条警告都带有fullMsg、shortMsg、warnNumber、isError以及行列定位信息;type_hints则记录每个表达式、绑定、核心类型、类型声明的类型提示与起止行列,供前端实现 hover 等交互。
每次compile调用前都会执行reset_compiler ()(重置警告缓冲、实验特性、Env缓存),保证多次编译之间互不污染——这是 Playground 能“反复编译不同代码”的关键设计。
端到端验证:playground_test.cjs 的集成测试
packages/playground/playground_test.cjs 提供了完整的端到端冒烟测试,覆盖:
- 以 CommonJS 方式
require("./compiler.js")并加载三个cmij.js; make()后打印初始配置,并通过setExperimentalFeatures(["LetUnwrap"])、setJsxPreserveMode(true)修改配置再打印;- 编译一段包含 React 组件(
@react.component、@react.componentWithProps)、JSX、Belt 管道(Belt.Array.map)、uncurried 函数与Console.log的代码; - 断言:编译结果
js_code非空、无errors、且至少产生一条 warning(测试代码中的未使用变量state恰好制造了这条警告),否则判为失败。
运行方式为yarn workspace playground test(对应node ./playground_test.cjs)。测试开头注释明确说明 Playground Bundle 是 UMD 模块——这解释了为什么既可以用require,也可以在浏览器中通过globalThis访问。
本地分发与线上发布
构建完成后,可通过两个脚本进一步分发:
yarn workspace playground serve-bundle:启动 packages/playground/serve-bundle.mjs 中的 h3 静态服务器(端口8888),以/v{版本号}/compiler.js、/v{版本号}/@rescript/belt/cmij.js等 URL 提供本地产物,同时提供/playground-bundles/versions.json版本列表接口——这适合在 Playground 前端开发阶段做本地联调。yarn workspace playground upload-bundle:调用 packages/playground/scripts/upload_bundle.mjs 发布到 Cloudflare R2。脚本会以v{rescript_compiler.version}作为目标目录,先打包compiler.js与全部packages/产物为tar.zst归档,再用rclone sync上传,并维护versions.json(按 semver 排序所有已发布版本)。运行前提是本机安装rclone与zstd;非 CI 环境下会先弹出[y/N]确认。
注意事项与限制
- 版本必须严格一致:
cmij.js的构建版本必须与compiler.js相同,否则序列化的接口信息无法匹配(源码注释与构建脚本均有明确要求)。 - 依赖由
rescript.json驱动:新增 Playground 库时应修改 packages/playground/rescript.json 的dependencies,构建脚本会自动为其生成cmij.js。 - 内建模块需单独打包:不要试图把运行时内建模块混入某个第三方库的
cmij.js,否则 Playground 内编译会因找不到内建模块而失败。 - API 存在版本演进:
api_version = "7"意味着旧版前端需要按该字段选择对应的绑定集;若使用config.experimental_features、config.jsx_preserve_mode等配置,需确保 Bundle 版本 ≥ 6。 - 语言支持范围:
Lang模块当前仅支持res,且 v5 起已移除.ml支持,convertSyntax目前也只能处理 ReScript → ReScript。
整体来看,packages/playground是“把 ReScript 完整编译能力搬进浏览器”的工程化落点:通过cmij.js机制解决依赖注入,通过jsoo_playground_main.ml提供稳定、可配置的 JS API,再配合playground_test.cjs、serve-bundle.mjs与upload_bundle.mjs完成验证、联调与发布,形成一条从源码到线上 Playground 的完整链路。
- 编译器
- 编程语言
- 开发工具
【免费下载链接】rescript-compiler
ReScript is a robustly typed language that compiles to efficient and human-readable JavaScript.
相关推荐
LiteRT-LM 源码构建指南:从 Bazel 构建 Android 双 JAR 到集成预编译依赖
LiteRT LM 源码构建指南:从 Bazel 构建 Android 双 JAR 到集成预编译依赖 导读 当你在开发 LiteRT LM Android 演示
大模型人工智能本地部署多模态三步解锁智慧教育平台电子课本:tchMaterial-parser让教材下载变得简单
三步解锁智慧教育平台电子课本:tchMaterial parser让教材下载变得简单 在教育数字化浪潮中,国家中小学智慧教育平台已成为师生获取教材资源的重要渠道
网页爬虫教育PyTorch Android 构建与集成指南:从 Maven 依赖到源码编译 libpytorch.so 的完整实践
PyTorch Android 构建与集成指南:从 Maven 依赖到源码编译 libpytorch.so 的完整实践 PyTorch 的 Android 集成
人工智能机器学习深度学习分布式训练模型编译
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考