- 前端
- 后端
- 知识管理
【免费下载链接】gitbook
The open source frontend for GitBook doc sites
导读
@gitbook/expr是 GitBook 开源仓库中负责"安全解析与求值用户自定义表达式"的核心工具包(见 packages/expr/README.md),它被用于文档站点中用户可配置的条件逻辑、模板占位符与动态内容渲染等场景。本文以 packages/expr/CHANGELOG.md 记录的版本演进为主轴,逐一还原每个版本变更背后的实现动机与源码细节,并结合 ExpressionRuntime 的核心 API、测试用例与工程化配置,帮助读者理解该包从 v1.0.0 发布到 v1.3.1 的完整设计脉络,同时掌握它的解析、求值、模板、变量提取与自动补全能力。
包定位:一个为"用户自定义表达式"而生的安全求值器
在深入版本历史之前,先明确这个包是什么。仓库根目录下的packages/expr是一个独立发布的 npm 包@gitbook/expr,其 package.json 中的描述与 README 完全一致:
Safely evaluate & parse user-defined GitBook expressions.(安全地求值并解析用户自定义的 GitBook 表达式)
"用户自定义"四个字点明了它的核心诉求:表达式内容由最终用户(或站点配置者)编写,不能像内部代码一样被信任,因此"安全求值"是整个包的设计基石。包的类型声明为 ESM("type": "module"),对外只暴露dist/index.js与dist/index.d.ts两个入口,并声明了"sideEffects": false——这是 v1.2.3 版本引入的重要工程化改动(下文详述)。
从依赖清单可以看到它的技术栈选型:
| 依赖 | 版本 | 职责 |
|---|---|---|
| acorn | ^8.15.0 | 标准模式下的 JS 表达式解析(生成 ESTree AST) |
| acorn-loose | ^8.5.2 | 宽松模式解析(容错处理不完整片段) |
| acorn-walk | ^8.3.4 | AST 遍历(自动补全功能使用) |
| escodegen | ^2.1.0 | 将 AST 节点重新生成为代码字符串 |
| eval-estree-expression | ^3.0.1 | 在受控环境下执行 ESTree AST,并支持变量提取 |
| assert-never | catalog: | 类型穷尽检查辅助工具 |
其中eval-estree-expression的依赖方式本身就走了一段演进之路:v1.2.4 改为"使用 npm 依赖",v1.3.1 又进一步从"钉死某个 GitHub commit"彻底切换到 npm registry 的^3.0.1正式版本——这一细节正是 CHANGELOG 主线之一,后文会展开。
核心 API 全景:ExpressionRuntime 的六大能力
包的公共出口集中在 src/index.ts,它 re-export 了errors、input-values、runtime、symbols、template、types、utils七个模块。其中 src/runtime.ts 定义的ExpressionRuntime类是绝对核心,围绕它展开的全部能力如下:
1. 求值三件套:evaluate / safeEvaluate / evaluateBoolean
evaluate(expr, inputs)是底层求值入口:先调用parse得到 ESTree AST,若存在invalidNodes(非表达式语句)则抛出ExpressionError,否则交给eval-estree-expression的evaluate.sync执行,并开启functions: true与withMembers: true两个选项——前者允许调用受限函数,后者允许成员方法调用(这正是数组、字符串方法的来源)。任何异常都会被包装为ExpressionError抛出。
safeEvaluate(expr, inputs)是面向生产环境的"不抛异常"版本,返回值是一个判别联合(discriminated union):
- 成功时返回
{ value: unknown }; - 失败时先通过注入的 logger 记录
Error while evaluating expression ...,再返回{ value: undefined, error: ExpressionError }。
evaluateBoolean(expr, inputs)进一步把结果收窄为布尔值:空字符串或纯空白表达式直接返回true(视为"无条件成立"),求值出错返回false,否则对结果做Boolean()转换。evaluateBooleanAll(expressions, inputs)则把一组条件当作 AND 逻辑串联,全部为真才返回true,空数组返回true——这组 API 非常适合"条件列表"类的配置场景。
2. 解析与宽松解析:parse
parse(expr, { loose })返回ExpressionParserResult,即{ result: Expression; invalidNodes: ExpressionStatement[] }:
- 标准模式下使用 acorn 的
parse,loose: true时改用acorn-loose的parseLoose,用于容忍语法不完整的输入(例如编辑器场景下的半成品输入); - 从 AST 中提取第一个
ExpressionStatement作为result,其余非表达式语句进入invalidNodes,但ImportDeclaration、ExportNamedDeclaration等模块声明会被filterOutModuleDeclarationStatement过滤掉; - 语法错误会被
createExpressionErrorFromSyntaxError转换为带location(行/列)与token的ExpressionError,其中getTokenAtLoc用 acorntokenizer重新扫描源码,精确定位出错 token——这为上层编辑器提供了良好的错误提示基础。
parse拒绝一切非表达式语句,这是安全性的第一道闸门:const a = 1;、while (1) {}这类语句在测试 src/tests/runtime.test.ts 中均被验证为抛出ExpressionError。
3. 模板能力:parseTemplate / evaluateTemplate
parseTemplate(template)(见 src/template.ts)用正则/\{\{(.*?)\}\}/gs把模板字符串切分成text与expression两类片段,每段都带start/end偏移量;表达式内容会被trim()。evaluateTemplate(template, inputs)把每个{{ ... }}片段求值后,经formatExpressionResult(见 src/utils.ts)格式化为字符串拼接输出:字符串原样返回,数字/布尔转字符串,null/undefined返回默认值(默认空串),其他类型(如对象、数组)返回默认值。测试 src/tests/template.test.ts 验证了Hello {{ user.name }}!→Hello John!的完整链路。
4. 变量提取:getVariables(v1.3.0 引入)
getVariables(expr)是 v1.3.0 的 Minor 变更新增能力,它调用eval-estree-expression的variables()函数,返回表达式中引用的变量路径数组。从测试可以看到其行为细节:
isBetaUser === true→['isBetaUser'];user.role === "admin"→['user.role'](成员表达式按点路径展开);products.includes("productA") && userSegments.alpha→['products.includes', 'userSegments.alpha'](方法调用保留方法名);- 表达式非法时返回空数组。
这一能力对"从表达式反推依赖的输入字段"类需求非常有用,例如在编辑器里高亮未定义的变量,或在服务端做输入裁剪。
5. 自动补全:autocomplete
autocomplete(expr, cursorOffset, context)接收一个SymbolsTable作为上下文,返回{ suggestions }。建议类型(见 src/types.ts)分为三类:
symbol:可用的变量符号(来自符号表);literal-value:字面量候选值,可能是直接值或来自数组枚举的成员值;operator:受支持的运算符,带说明文本。
受支持的运算符在SUPPORTED_BINARY_OPERATORS、SUPPORTED_LOGICAL_OPERATORS、SUPPORTED_CONDITIONAL_OPERATORS三张表中显式声明:二元比较==、!=、===、!==、<、<=、>、>=、in,逻辑运算符&&、||,以及三元条件运算符?。这种"白名单式"的运算符声明本身就是安全设计的一部分——求值面被显式收窄。
6. 符号表:SymbolsTable
SymbolsTable(见 src/symbols/symbols-table.ts)为自动补全与输入校验提供结构化的变量元信息:
inferSymbolFromValue(value):从实际 JS 值推断符号定义(数组要求元素类型一致,否则抛SymbolError);inferSymbolFromJSONSchema(schema):从 JSON Schema 推断符号定义,支持string(含enum枚举与description描述)、number/integer、boolean、null、object(递归 properties)、array(递归 items)等类型;merge(other):合并两张表生成新表;getSymbolInfo(path)/getMatchingSymbolsKeys(path):按点路径查询,支持通配符*匹配。
配套的 src/input-values.ts 提供了inferDefaultInputValuesFromObjectJSONSchema,可按 JSON Schema 生成默认输入值:布尔默认true、数字/整数默认1234、字符串取enum首项否则'default'、null默认null、数组默认[](或按 items 递归)、对象递归生成——这套默认值机制常用于"表达式试运行"场景。
版本演进详解:CHANGELOG 逐版本还原
以下按 CHANGELOG.md 的时间线逐条展开,每条版本变更都与当前仓库中的实现证据对应。
v1.0.0:包的诞生——"帮助求值用户自定义表达式"
Major Changes:Publish gitbook/expr package to help evaluate user defined expressions.
这是包的初始发布版本,确立了全部核心设计:ExpressionRuntime的 parse/evaluate/safeEvaluate 骨架、acorn 解析 + eval-estree-expression 求值的双层架构、以及"只接受表达式语句、拒绝任意语句"的安全边界。从 runtime.test.ts 的INVALID_EXPRESSSIONS用例可以看到这条边界被测试钉死:t}=d(语法错误)、const a = 1;(非表达式语句)、while (1) {}(语句)、[1, 2, 3].map(() => { while (1) {}})(嵌套危险语句)在evaluate下全部抛出ExpressionError,在safeEvaluate下全部返回{ error }。
v1.1.0:修复打包 + 数组 every/some 方法支持
两个变更并存:
- Patch:Add support for every/some array methods——这是 std lib 扩充的早期动作。当前测试中
reviews.every(review => !!review.status)、reviews.every(review => review.status === "approved")的用例即源于此。这类方法之所以可用,是因为evaluate.sync开启了withMembers: true,数组成员方法在受控白名单内被执行; - Minor:Fix bundling of gitbook/expr package——打包问题在 v1.1.1、v1.2.0、v1.2.3 中反复出现,可见这是发布工程化的持续痛点。
v1.1.1:修复 eval-estree-expr 命名导入
Patch:Fix eval-estree-expr named import.
在 runtime.ts 中可以看到最终形态:import evalESTreeExpr from 'eval-estree-expression'; const { evaluate, variables } = evalESTreeExpr;——即先默认导入再解构命名成员。这个修复说明早期版本在 ESM 环境下直接具名导入时曾遇到互操作问题,最终统一收敛为"默认导入 + 解构"的稳妥写法。
v1.2.0:修复 exports 声明
Minor:Fix exports in gitbook/expr package.json.
当前 package.json 的exports字段是一个干净的单一入口映射:"."下types指向./dist/index.d.ts、default指向./dist/index.js。v1.2.0 修复的正是这个字段的形态——在 Node ESM/TypeScript 双环境下确保类型解析与运行时解析一致。
v1.2.1:新增 dev 开发脚本
Patch:Add dev script for @gitbook/expr.
对应 package.json 中的"dev": "bun run build -- --watch ./src"——基于 tsdown 的 watch 模式持续重建 dist,使包开发获得即时反馈。这标志着包的工程化从"一次性构建"走向"可迭代开发"。
v1.2.2:重新发布
Patch:Republish packages.
一次纯发布运维动作,无代码变更。值得注意的是仓库的发布脚本设计:"publish-to-npm": "../../scripts/publish-if-new.sh",脚本位于 scripts/publish-if-new.sh,语义为"仅在新版本时发布",避免重复发布与版本覆盖。
v1.2.3:标记 sideEffects 并修复所有包打包
Patch:Mark as sideEffects, fix all package bundles.
当前 package.json 中"sideEffects": false即此变更的成果。对 ESM 生态而言,这一声明让打包器(webpack/Rollup 等)可以在 tree-shaking 时安全删除未使用的导出,是发布质量的重要提升;同时"fix all package bundles"说明这是一次横跨 monorepo 多个包的打包修复。
v1.2.4:NPM Trusted Publishing + 依赖改走 npm
Patch:Use NPM Trusted publishing for publishing the package. Use NPM dependency for eval-estree-expression.
两个动作都与发布供应链相关:
- Trusted Publishing:利用 npm 的 OIDC 信任发布机制替代长期有效的访问令牌,属 CI 安全加固;
- 改用 npm 依赖:
eval-estree-expression从特殊来源(如 git URL 或本地路径)切换为 npm 依赖,为后续 v1.3.1 的彻底正规化埋下伏笔。
v1.2.5:扩展标准库
Patch:Extend gitbook/expr std lib with some additional methods.
从当前 runtime.test.ts 的用例矩阵可以观察到 std lib 目前覆盖的方法面:
- 数组:
includes、map、every(some在 v1.1.0 加入); - 字符串:
startsWith、endsWith、includes、toLowerCase、toUpperCase、trim。
例如user.role.startsWith("ad")、[1, 2, 3].map(n => n * x)(配合外部输入变量x)都在测试中被验证。这些方法由eval-estree-expression的标准库提供,ExpressionRuntime通过functions: true与withMembers: true开启——注意这是一个显式收窄的白名单,而非完整 JavaScript 运行时,这正是"安全求值"的体现。
v1.3.0:实现 getVariables
Minor:Implement a getVariables function for ExpressionRuntime.
这是 CHANGELOG 中唯一被标记为 Minor(新功能)的变更,实现位于 runtime.ts。它调用eval-estree-expression的variables()并复用与求值一致的选项(functions: true、withMembers: true、generate: escodegen.generate),解析失败时记录日志并返回空数组。配套测试describe('getVariables')覆盖了单变量、多变量、成员表达式、嵌套成员 + 方法调用四类场景,并在generate测试中留下了"由 AST 还原原始表达式"的describe.skip占位——ExpressionRuntime.generate目前仍抛出Not yet implemented,属于未来能力预留。
v1.3.1:彻底移除脆弱的 git/tarball 依赖
Patch:Depend on
eval-estree-expressionfrom the npm registry (^3.0.1) instead of a pinned GitHub commit. The published3.0.1release is built from the exact commit the package was pinned to, so the code is unchanged — this only removes the fragile git/tarball dependency so consumers install it from npm like any other package.
这是当前最新版本(1.3.1),其变更本身是一次"零行为差异"的工程化收敛:发布到 npm 的3.0.1正是此前钉死的那个 GitHub commit 的构建产物,代码不变,但依赖方式从"易碎的 git/tarball 引用"变为标准的^3.0.1npm 范围依赖(见 package.json 的 dependencies)。对下游消费者而言,安装路径、锁文件语义与解析稳定性都得到改善——这也呼应了 v1.2.4 的中间步骤。
安全设计的三道防线
纵览整个演进,安全求值并非单一机制,而是层层叠加:
- 语法层:acorn/acorn-loose 只解析,不执行任何副作用;
parse强制只接受第一个ExpressionStatement,其余语句进入invalidNodes并触发ExpressionError; - 语义层:
eval-estree-expression的求值器配合functions: true/withMembers: true白名单,只开放 std lib 中预先批准的方法与运算符(比较、逻辑、三元、in),while等控制流语句、import/export模块声明在解析阶段就被filterOutModuleDeclarationStatement排除; - 错误处理层:
ExpressionError(见 src/errors.ts)携带location(acorn 的Position)与token,上层 UI 可以据此定位并高亮出错位置;safeEvaluate保证任何异常都不会穿透到调用方,而是以{ error }结构返回,配合可注入的Logger(debug/info/error,默认console)输出诊断日志。
工程化与开发工作流
当前仓库中与该包配套的开发/发布工作流:
- 构建:
bun run build(tsdown 打包到dist/),bun run build -- --watch ./src为开发模式; - 类型检查:
bun run typecheck(tsc --noEmit,基于@tsconfig/strictest严格配置); - 单元测试:
bun run unit(bun test),测试文件位于 src/tests/(runtime、template、autocomplete、input-values、symbols 均有覆盖); - 发布:
publish-to-npm走 scripts/publish-if-new.sh,配合 npm Trusted Publishing(v1.2.4 引入); - 对外产物:
files仅包含dist、README.md、CHANGELOG.md,保证发布体积最小化。
结语:从 CHANGELOG 读懂一个安全求值库的设计沉淀
@gitbook/expr的 CHANGELOG 虽然只有短短十一条记录,却完整刻画了一个开源包的成长曲线:从 v1.0.0 确立"安全求值用户表达式"的架构骨架,到 v1.1.x–v1.2.x 反复打磨打包与依赖互操作,再到 v1.3.0 的getVariables新能力与 v1.3.1 的依赖供应链正规化。每一次 Patch 都不是孤立的修修补补,而是与 runtime.ts、symbols-table.ts、package.json 中的实现细节一一对应。对于想在文档站点、配置引擎或低代码场景中安全嵌入用户表达式的开发者而言,这个包的演进史本身就是一份"如何把表达式求值做成生产级能力"的参考样本。
- 前端
- 后端
- 知识管理
【免费下载链接】gitbook
The open source frontend for GitBook doc sites
相关推荐
BasicSR 功能演进与实现解读:从 v1.0.0 到 ECBSR、SwinIR、BasicVSR 与 NIQE 的完整路线图
BasicSR 功能演进与实现解读:从 v1.0.0 到 ECBSR、SwinIR、BasicVSR 与 NIQE 的完整路线图 本文基于 BasicSR 仓库
人工智能深度学习计算机视觉图像处理视频处理Hutool表达式解析:动态表达式求值引擎
Hutool表达式解析:动态表达式求值引擎 还在为Java项目中复杂的动态表达式计算而烦恼?还在手动编写繁琐的解析逻辑?Hutool表达式解析模块为你提供了一套
后端开发工具Flowbite 版本演进全解析:从 v1.0.0 到 v4.0.2 的组件库发展路线图
Flowbite 版本演进全解析:从 v1.0.0 到 v4.0.2 的组件库发展路线图 Flowbite 是基于 Tailwind CSS 的开源 UI 组件
UI组件前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考