 深度解析:将 TE 片段编译为字符串与 refs 的编译阶段)
tamedevil te.compile() 深度解析将 TE 片段编译为字符串与 refs 的编译阶段【免费下载链接】crystal Graphiles Crystal Monorepo; home to Grafast, PostGraphile, pg-introspection, pg-sql2 and much more!项目地址: https://gitcode.com/gh_mirrors/cry/crystalte.compile(fragment)是 tamedevil 库中把 TETamed Evil片段编译成可直接求值字符串、但不求值的核心 API。它返回{ string, refs }结构是调试、快照测试与理解 tamedevil 代码生成流水线的关键入口。读完本文你将掌握te.compile()的调用方式、返回值语义、与te.run()/te.ref()的分工以及底层序列化实现原理结合源码与测试用例验证。一、te.compile() 是什么编译但不求值tamedevil 的定位是驯服 eval——让开发者用 tagged template literal 安全地动态生成高性能 JS 代码参见 tamedevil 项目介绍。整套流程分为两个阶段构建阶段用te\...与te.ref()、te.lit() 等辅助函数构造 TE 节点树编译阶段te.compile()把 TE 节点树序列化为代码字符串同时收集所有外部值引用执行阶段te.run()基于编译产物用new Function求值。te.compile()就处于第 2 阶段它Builds the TE fragment into a string ready to be evaluated, but does not evaluate it即把 TE 片段编译成可求值的字符串但不执行。文档原文的示例const fragment tereturn ${te.ref(1)} ${te.ref(2)}; const result te.compile(fragment); assert.deepEqual(result, { string: return _$$_ref_1 _$$_ref_2, refs: { _$$_ref_1: 1, _$$_ref_2: 2 }, });可以看到te.ref(1)与te.ref(2)没有把字面量1、2直接拼进字符串而是被替换为占位标识符_$$_ref_1、_$$_ref_2真正的值存放在返回对象的refs字段中——这正是通过闭包传值、而非序列化注入这一安全设计的体现。二、返回值结构string与refste.compile()返回一个对象包含两个字段字段类型含义stringstring编译后的函数体代码字符串其中所有外部值都被替换为占位标识符refs{ [key: string]: any }占位标识符到真实值的映射即求值时需要通过闭包参数传入的值源码 utils/tamedevil/src/index.ts#L296-L328 中compile()的实现如下function compile(fragment: TE): { string: string; refs: { [key: string]: any }; } { const refs: { [key: string]: any } Object.create(null); const refMap new Mapany, string(); const varMap new Mapsymbol, string(); let str serialize(fragment, refs, refMap, varMap); // ... 临时变量声明前缀拼接 ... const string isDev ? str.replace(/\n\s*\n/g, \n) : str; return { string, refs }; }几个值得注意的实现细节refs使用Object.create(null)创建避免__proto__等原型链属性干扰也保证任意键名都能安全存储refMap是Mapany, string同一个值只分配一个标识符重复引用同一对象时复用同一标识符去重varMap用于临时变量te.tempVar()/te.tmp()引入的临时变量_$_tmp1、_$_tmp2...会以let _$_tmpN;的形式声明在代码字符串头部开发模式净化当GRAPHILE_ENV development见源码 utils/tamedevil/src/index.ts#L25-L26 的isDev时会压缩连续空行便于阅读调试。占位标识符的命名规则标识符的分配逻辑在makeRef()utils/tamedevil/src/index.ts#L199-L222未命名 ref按出现顺序生成_$$_ref_${refCount}_$$_ref_1、_$$_ref_2...命名 refte.ref(value, name)传入名称时优先使用该名称若与已有 refs 键冲突则通过findAvailableName()追加数字后缀如source0、source1直到不冲突为止数量上限单个 TE 语句最多支持 65535 个占位符超出会抛出明确错误提示改用数组或对象聚合多个值。三、运行时的协作关系compile → new Functionte.compile()虽不求值但它正是te.run()求值的中间产物。源码_runCore()utils/tamedevil/src/index.ts#L881-L910展示了完整调用链const compiled compile(fragment); const argNames Object.keys(compiled.refs); const argValues Object.values(compiled.refs); const fn newFunctionany[], TResult(...argNames, compiled.string); return fn(...argValues);即先compile()得到{ string, refs }再把refs的键作为函数参数名、refs的值作为实参通过new Function(...argNames, compiled.string)构造并立即调用。也正因为如此te.compile()返回的string必须是合法的函数体代码通常需要包含return语句才有返回值参见 te.run() 文档。对照一下 te.run 文档示例const fragment tereturn 1 2; const result te.run(fragment); // 3它等价于te.compile(fragment)得到{ string: return 1 2, refs: {} }无 refs 时参数列表为空随后执行该函数体。四、调试与测试中的典型用法te.compile()的两大典型场景正如文档所说debugging 与 tests。场景一测试断言编译产物单元测试 utils/tamedevil/tests/general.test.ts#L3-L10 中最基础的编译断言it(basic, () { expect(te.compile(tereturn 1)).toMatchInlineSnapshot( { refs: {}, string: return 1, } ); });没有 ref 时refs为空对象string就是原样代码。场景二验证多个 ref 的编译结果同一个测试文件 general.test.ts#L15-L60 中a few refs 用例把-Number.MAX_SAFE_INTEGER、-Number.MAX_VALUE、空字符串、含引号反斜杠的怪异字符串、布尔值、null、undefined、复杂对象等十种值全部通过te.ref()注入编译后refs依次生成_$$_ref_1~_$$_ref_10string中只出现这些标识符。这印证了ref 引用传值by reference而非序列化——复杂对象、函数等都可以安全传递。const frag tereturn [ ${te.ref(-Number.MAX_SAFE_INTEGER)}, ${te.ref(COMPLEX_OBJECT)} ]; expect(te.compile(frag)).toMatchInlineSnapshot(/* refs: { _$$_ref_1: ..., _$$_ref_2: { a: 1, b: { c: 3 } } }, string: return [ _$$_ref_1, ... _$$_ref_2 ] */);场景三命名 ref 的可读性general.test.ts#L231-L276 的 named refs 用例展示了传入名称后的效果——refs的键变为maxSafeInt、maxValue、complex等可读名称string中直接出现这些名字。这对于生成可读、可调试的函数源码如 te.ref() 文档 中给 ref 起别名便于调试函数文本的建议非常有价值。场景四调试内部实现源码中的debug()函数utils/tamedevil/src/index.ts#L146-L159本身就用到了te.compile()先编译得到string与refs再构造函数并toString()输出带行号的源码。可见te.compile()也是 tamedevil 自查、调试的工具。五、序列化原理serialize 与节点类型te.compile()的核心工作是serialize()utils/tamedevil/src/index.ts#L235-L290。TE 节点通过 Symbol 标记$$type区分类型见 utils/tamedevil/src/index.ts#L31-L78 的节点接口定义节点类型说明编译行为QUERY由te\...生成n 为节点数组逐个序列化子节点并拼接RAW原始代码文本作者自己写的代码原样输出开发模式按缩进重排REFte.ref()注入的外部值分配/复用标识符值记入refsVARIABLEte.tempVar()临时变量分配_$_tmpN声明提升到代码头部INDENTte.indent()缩进包装仅开发模式生产模式下抛出错误开发模式下缩进输出关键点RAW节点只应由作者自己书写的代码产生如te\return ...中的静态文本或经te.lit()字面量内联、te.substring()字符串转义等安全通道生成外部动态值一律走REF通道。这正是 tamedevil 防止代码注入的核心——te...模板中出现的所有插值都必须是通过合法 API 构造的 TE 节点直接插入普通值会抛出异常参见 [te\... 文档](https://link.gitcode.com/i/19e1e65ba57f5f958908b13c20398313)。六、compile 与 run 的选择何时用哪个te.run(fragment)一步到位编译并立即求值返回结果te.compile(fragment)只编译不求值返回{ string, refs }适合编写单元测试时断言生成的代码字符串与 refs 映射是否符合预期tamedevil 自身测试即如此调试时人工检查生成的代码是否合理、是否存在多余的占位符或命名冲突在需要先查看代码、再决定是否执行的场景如代码审查、日志输出中保持安全——字符串本身无副作用。从测试 general.test.ts#L190-L192 还可以看到两者的组合形态expect(te.runreturn ${te.lit(1)}${te.ref(2)}).toEqual(3);te.run支持直接接收模板字符串内部先经te()构造 TE再走 compile → new Function 的完整链路。七、导入方式与使用前提te.compile作为te标签函数的属性导出只需要导入te即可使用参见 Importing 文档// ESM import { te } from tamedevil; // CommonJS const { te } require(tamedevil);安装yarn add tamedevil # 或 npm install --save tamedevil使用前提与限制compile()的入参必须是合法 TE 节点te\...或te.ref()等构造的节点传入普通 JavaScript 值会抛出[tamedevil] Invalid expression... 错误返回的string是函数体代码若后续交给new Function求值需要函数体语法合法例如包含return单个片段占位符数量上限为 65535refs是无原型对象键名即标识符命名 ref 时应避免与保留字冲突te.ref(value, null)等会被拒绝测试 general.test.ts#L194-L229 覆盖了null、true、false、debugger、undefined等保留字场景。八、小结te.compile(fragment)是 tamedevil 代码生成流水线中编译环节的公开窗口它把 TE 节点树序列化为{ string, refs }二元组前者是可直接求值的函数体代码后者是外部值的闭包映射。它不求值、零副作用因而天然适合测试断言、调试检查与代码审计其底层由serialize()makeRef() 节点类型系统支撑配合te.ref()、te.lit()等安全通道在保留new Function高性能动态代码能力的同时把代码注入风险收敛在作者可控的范围内。若要深入了解其姊妹 API可继续阅读 te.ref()、te.run() 及 te... 标签函数 文档。【免费下载链接】crystal Graphiles Crystal Monorepo; home to Grafast, PostGraphile, pg-introspection, pg-sql2 and much more!项目地址: https://gitcode.com/gh_mirrors/cry/crystal创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考