Commit Graph
7 Commits
Author SHA1 Message Date
what 0c947dde70 docs: 加一篇函数流走向
call-flow.md 讲的是三个阶段各自在做什么,缺的是**函数之间怎么串起来的**——
想改代码、想知道加一层该加在哪,没有一处能查。

docs/flow.md 七张图:

  全景            公开 API → 注册表 / 取 VM / 调用 三条链
  runner 三态     Script / Instance / staticTarget 的 acquire+finish 差别,
                  「同一实例串行」就是 acquire 到 finish 之间一直持着 i.mu
  newVM 内部      为什么 scope 是显式参数而不是从 ctx 嗅
  作用域进 VM      WithScope → vmGlobals → bind → lazyGlobal → freeze 五跳,
                  这是全库最容易看不清的一条链
  invoke 内部     含 defer 的注册顺序(finish → stop → recover,后进先出)
  lookup 三级     exports → ctor 静态 → 全局兜底,解释了 Script.Has 为什么
                  对静态方法也返回 true
  编译链          Prepared 为什么不是优化开关而是正确性要求

写的时候实地核对了每条断言,抓出三处我自己写错的:

  1. 图上把 Bundle → FinalizeBundle 画成两步,实际 Bundle 末尾自己就调了
  2. 把「省 44% 常驻内存」归因给惰性注入——那是关掉 console 的收益
  3. 「只读全局赋值静默失败」只对全局本身成立,freeze 拷出来的嵌套只读属性
     (store.Set = null)实测是抛错的

顺带修一处上一轮改名的残留:engine.go 的注释还指着 lazyglobal_test.go。
2026-09-10 16:24:18 +08:00
what 6477b27ef4 docs: 校对 README 和 docs,改掉三处跟代码对不上的
不是全面重写——文档里不指向具体源文件,所以上一轮的改名没波及它们。逐条核对
「文档提到的 API」和「实际导出」之后,只有三处不符:

  README 的哨兵错误列表   多了 ErrUnsupportedSignature(这轮删了),
                          少了 ErrInterrupted(一直都有)
  call-flow.md 的 freeze  说白名单「逐层拷贝成只读对象,否则同一个 Go map 会被
                          所有 VM 共享」——那只对 map[string]any 成立。结构体
                          指针和 slice 走 rt.ToValue,是**共享同一个对象**的。
                          扩展走的正是这条路,不该被当成隔离保证。
                          (doc.go 里同一处上一个 commit 已经改过)
  lifecycle.md 的状态图   写着 [*] --> 池中: newVM(),但 newVM 产出的 VM 是
                          直接借出去用的,回池要等 release

错误分类那张表(KindTimeout/KindCanceled/KindPanic/KindRuntime)核对过,没有
KindSignature,不受这轮删除影响。
2026-09-10 15:51:48 +08:00
what 98eb734caa docs: 把能力面摆出来,更新性能数字
起因是「不知道有什么功能」——公开 API 有 14 个类型、17 个 With* 选项,全部平铺
在同一层文档里,看不出哪些是必须懂的。

doc.go 和 README 顶部都加了分层的能力清单:核心五个概念(Engine / Script /
Instance / Scope / Extension,覆盖九成用法),加上「用不到就不用看的」四类。
同时写清楚明确**不做**的三件事:不代管实例生命周期、没有事件循环、不提供沙箱隔离
(脚本能拿到你放行的 Go 对象,它们的方法是真能调的)。

性能数字重测了一遍。InstanceCall 454 → 300 ns——删掉 lastUsed 省下的两次
time.Now() 兑现了。

顺带加了一句测量方法的提醒:这些数字取的是多轮**最小值**。我这轮一度以为
Script.Call 退化了 27%,逐个 commit 二分下去发现跳变落在一个只删死代码和改注释
的 commit 上——那不可能影响调用路径。加大样本后两边最小值持平,是机器噪声。
中位数会被离群值带偏,微基准在有别的负载时能飘 30%。
2026-09-10 15:44:19 +08:00
what ad722b12f9 fix(esm): Vendor 支持子路径,不然 es-toolkit/compat 这种装不了
有些包把东西放在子路径下(es-toolkit 的 toString 只在 compat 里,主入口没有),
而摊平之后原包的 exports 映射就没了,import "es-toolkit/compat" 解析不到。

现在直接写子路径就行:

    esm.Install(ctx, "es-toolkit/compat", "app/node_modules")

拉的是根包,摊平的是子路径,落到 node_modules/es-toolkit/compat/。根包和子路径
可以共存——子路径目录嵌在根包目录里,而最小 package.json 不写 exports,
所以解析器认得出来。

npm.SplitPath 负责拆名字,scoped 包名自带一个斜杠所以前两段才是包名。
2026-09-07 10:58:34 +08:00
what 00d95c496f feat(esm/npm): 用 Go 拉依赖树,加库不再需要 node
零依赖的包下个 tarball 就能用,有依赖的得先解析依赖树——读 semver 范围、
查注册表定版本、递归。这个包把那件事用 Go 做了,于是整条链没有 node:

    esm.Install(ctx, "qs", "app/node_modules")
    // qs v6.16.0  打进 50 个文件 -> 73.3 KB(依赖树 19 个包 1.7 MB)

= npm.Fetch(拉依赖树到临时目录)+ Vendor(摊平成一个文件)。

刻意不做的(它不是 npm):

  - 不跑安装脚本。那是供应链攻击的主要入口,而纯 JS 库没有编译步骤
  - 不管 devDependencies / peerDependencies / 平台二进制
  - semver 只实现 ^ / ~ / 精确 / x / >= 这个子集
  - 只平铺不嵌套

碰上支持不了的(复合范围、主版本冲突)明确报错并指向 npm + Vendor,
不猜版本——猜错了装出来能跑但行为不对,比装不上难查。

子集划得这么小是有依据的:抽 8 个常见包的 44 个传递依赖统计,^ 占 95%,
~ 和精确各一两处,主版本冲突 0 个。

安全上做了两件事:校验注册表给的 sha512(中间的缓存代理、私有源镜像
是真实存在的),以及挡住 tarball 里带 ../ 的路径。

测试全部走内存假注册表,不碰网络:依赖树平铺、共同依赖只装一次、
版本冲突报错、校验和不符、目录穿越。semver 那组表驱动——写这组时抓到
一个真 bug:1.2.x 被映射成了 ^1.2.0,只锁主版本,实际该锁到 1.2。
2026-09-07 10:11:51 +08:00
what efb3141734 feat(esm): Vendor 把装好的 npm 包连同依赖摊平成单文件
解决的是「脚本要用第三方库,但目标机器上没有 node」。

npm 真正干的活是解析依赖树——读 semver 范围、查注册表定版本、递归、处理冲突。
这步绕不开,得在有 node 的机器上做一次。但做完之后依赖树就是死数据了,用
esbuild 摊平成一个文件,发布物里只带那一个就够:

    qs         v6.16.0   打进  47 个文件 -> 73.6 KB   (原 19 个包 1.7 MB)
    es-toolkit v1.52.0   打进 219 个文件 -> 51.9 KB

产物是最小的 node_modules 布局,脚本照常 import,写法完全不变。

两个实现细节:

  - 入口不能直接写包名,esbuild 的 EntryPoints 是文件路径。所以造一段转发
    源码当 stdin 入口,包名放进 import,才走正常的 node_modules 解析。
  - 转发源码里写 export { default } 时,只有具名导出的包会报错(ESM 原生的
    很多是这样),退回去用只带具名导出的版本重打一次。

打包目标从 Loader 里提成了共用常量:摊平出来的库必须跟脚本同一档,
否则库能打出脚本引擎跑不了的语法。

零依赖的包不用这个——直接下 tarball 解开就行,README 里记了命令。
2026-09-07 09:54:18 +08:00
what c2ce37ad2d docs: README 与执行流程、生命周期两篇
README 分四段:脚本怎么写、Go 侧怎么调、安全边界与可靠性、性能。
docs/call-flow.md 讲源码怎么变成可执行的、一次调用经过哪些环节;
docs/lifecycle.md 讲 Engine / Script / VM / Instance / 作用域各活多久。

性能一节的每一行都标了对应的基准名,数字过时了可以自己重跑。原来有一张
「循环里访问 Go 对象」的表没有对应的基准测试,数字无法复现,换成了
Benchmark绑定_* 的实测结果。内存那张表仍是手工测的,已在旁边注明。

挑第三方库那节记了两条实测结论:esbuild 的 target 只降级语法、不补全局
对象,所以库只要用了 structuredClone 或定时器就是运行期才炸;CommonJS
包摇不动,同一组功能 lodash 打出 419 KB 而 es-toolkit 只要 7 KB。
2026-09-05 22:13:57 +08:00