☰
@gitbook/expr 表达式引擎演进与实现解析:GitBook 安全求值库从 v1.0.0 到 v1.3.1 的完整路线图
2026/10/1 2:01:45 网站建设 项目流程
  • 前端
  • 后端
  • 知识管理

【免费下载链接】gitbook

The open source frontend for GitBook doc sites

项目地址:https://gitcode.com/gh_mirrors/gi/gitbook
点击查看免费下载

导读

@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.4AST 遍历(自动补全功能使用)
escodegen^2.1.0将 AST 节点重新生成为代码字符串
eval-estree-expression^3.0.1在受控环境下执行 ESTree AST,并支持变量提取
assert-nevercatalog:类型穷尽检查辅助工具

其中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 oneval-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 的中间步骤。

安全设计的三道防线

纵览整个演进,安全求值并非单一机制,而是层层叠加:

  1. 语法层:acorn/acorn-loose 只解析,不执行任何副作用;parse强制只接受第一个ExpressionStatement,其余语句进入invalidNodes并触发ExpressionError;
  2. 语义层:eval-estree-expression的求值器配合functions: true/withMembers: true白名单,只开放 std lib 中预先批准的方法与运算符(比较、逻辑、三元、in),while等控制流语句、import/export模块声明在解析阶段就被filterOutModuleDeclarationStatement排除;
  3. 错误处理层: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

项目地址:https://gitcode.com/gh_mirrors/gi/gitbook
点击查看免费下载

相关推荐

上一篇:SGLang学术研究:NeurIPS 2024论文深度解读与实现原理
下一篇:golangci-lint v2 系列版本演进全解读:从 2.0 到 2.13 的关键变更与实践指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询