返回博客
npmtest262Windowswidgetsmilestone

真实的 npm 包现在能编译了:axios、zod、express —— 以及一次一致性扫尾

上一篇文章在 v0.5.875 收尾,主线是 GC 的故事 — 填平 aya_koto 基准所暴露出的差距。那篇文章讲的是赢下一个基准。这一篇讲的是另一种工作:从 v0.5.875 到 v0.5.1146 之间大约 270 个版本,在约四周里发布,几乎没有一个是以基准数字作为头条。主题从“在微基准上跑得快”转向了 “让真实世界的 TypeScript 和真实的 npm 包真正能编译并运行”。外加一次完整的 Windows 视觉翻新,以及一路上一堆新 widget。

下面是这一窗口里发布的内容,按它们实际服务的目的分组。

真实的 npm 包现在能编译了

贯穿这一窗口最大的一条主线,是一次让流行 npm 包编译成原生二进制并通过行为测试的扫尾 — 不只是“链接时不报错”,而是运行并产生正确输出。如今通过 perry.compilePackages 能工作的清单包括 axios、jose、zod v4、vitest、express、fastify、@hono/node-server、dayjs、chalk、ms、debug、lodash、ethers、argon2 和 Colyseus

每一个都因各自的原因失败,每一处修复也都是它自己的小故事:

  • zod v4Cannot read properties of undefined (reading 'onattach') 崩溃。根因(v0.5.1144,#4698):当 F 是从另一个模块导入的函数时,new F() 静默产生一个空对象 — 构造函数体从未运行,于是每个 $ZodCheckMinLength 风格的 check 回来时都被剥掉了它的 _zod 属性。
  • axios + jose 需要 Perry 当时还没有的加密与压缩能力:zlib.createBrotliDecompresscrypto.subtle.wrapKey/unwrapKey、用于 AES-GCM 的 subtle.generateKey / encrypt / decrypt,以及 randomFillSync(v0.5.972–976)。
  • fastifywait_for_promise 里一个一秒的轮询超时上死锁;我们用一个 condvar 等待替换了它,并让被拒绝的 promise 浮现为 HTTP 500 而不是挂起(v0.5.912)。
  • @hono/node-server 读不了 POST body — 在父级注册修复(v0.5.1142)之前,c.req.text() / .json() / .formData() 在 POST/PUT 上返回空。
  • chalk、ms、debug、express 撞上了同一种形态:一个带附加属性的可调用值chalk.redexpress() 加上 express.Router)。这一模式的三种变体在 v0.5.935 及其周边的 npm 扫尾中得到修复,外加 util.inherits + 一个 stream 原型脚手架来为 express 解阻(v0.5.990)。
  • dayjs 以压缩后的 bundle 形式发布,触及了 Perry 降级错误的 JS-classic 原型方法派发(Class.prototype.m = fn)(v0.5.924/932)。

在这一切之下,还有让 Perry 无法原生编译的包仍能运行的那一部分:V8-fallback 运行时在这一窗口里变成了真东西。它的 ModuleLoader 现在从一个内嵌的模块映射中读取,因此一个 fallback 二进制仍然是自包含的 — 运行时没有散落的 node_modules(v0.5.994)。createServer 桥接到一个真正的 hyper server(v0.5.999),并且 Response / Request / Headers 这些 Web Fetch 全局对象在 fallback 路径里存在(v0.5.1006)。还有编译期动态 import() — 字符串字面量 await import('./foo.ts') 在构建时解析 — 终于落地(v0.5.905,#100)。

一次 test262 一致性扫尾

另一条主导主线是一致性。我们针对 test262 子集雷达做了聚焦的几遍,把真实代码最倚重的那些内置项的指针拨动了:

built-ins/String         60.2% → 79.3%   (v0.5.1128)
built-ins/Array          61.5% → 72.5%   (v0.5.1127)
language/.../destructuring 41.6% → 53.9%  (v0.5.1143)

String 的跃升来自给每个 String.prototype 方法以泛型 this 派发,并修复 slice/substring 的索引强制转换。Array 的跃升是密集数组回调(forEach/map/filter/…)上的 thisArg、array-like 的 ToLength、规范操作顺序,以及零参数验证。解构吸纳了在普通、generator、async-generator、static 和 private 类方法上的参数解构。

在这些头条数字之外,还落地了一条长尾的正确性修复:JSON.parse 现在抛出真正的 SyntaxError(而非 TypeError)并拒绝尾随 token;它的 reviver 通过规范的 InternalizeJSONProperty 算法遍历;Object.prototype.toString 为 typed array、Symbol、BigInt、Map/Set/WeakMap/WeakSet/Promise/RegExp 正确打标;RegExp.prototype.toString 返回 /source/flags;async generator 拿到了它们 yield-awaits-operand 的语义。这些是子集雷达,不是完整套件 — Perry 仍在攀登 — 但这个月的攀登很陡。

Windows 走向 Fluent

Windows 得到了一次视觉翻新(#4681 系列)。Perry 的窗口现在默认接入现代 DWM 外观 — Mica backdrop、圆角,以及一个感知主题的标题栏 — 而公共控件通过 comctl32 v6 渲染,而不是 Windows 95 时代的默认。窗口过程现在处理 WM_DPICHANGED,因此当你把窗口在不同缩放的显示器之间拖动时,它保持清晰,而不是被位图拉伸。

关键在于,这一切都没有重新引入旧的 #1542 “resize 后的黑色区域”回归:客户区仍以不透明绘制,而全帧的 Mica/Acrylic 透视模糊保持为一个显式的 app.setVibrancy(...) 选项。还有一个新的 --target windows-winui 后端脚手架(WinUI 3),给想要完全现代栈的应用使用,以及一个小但真实的修复,让 perry compile main.ts -o main 在 Windows 上产出 main.exe,这样 PowerShell 才会真正启动它(v0.5.1146)。

新 widget,每个平台

就在最后一天里落地了两个 widget,两个都覆盖 Perry 所瞄准的每个 UI 平台:

  • DatePicker#4772)— 一个紧凑的、字段式的日期控件:macOS 上是 NSDatePicker,iOS/visionOS 上是 UIDatePicker(.compact),Windows 上是 SysDateTimePick32,Android 上是 android.widget.DatePicker,Linux 上是 GTK4。所有平台共用一套 TS 表面。
  • 拖放#4773)— 任何 widget 都可以作为文本/文件/URL 的放置目标和拖动源,映射到 NSDraggingDestination(AppKit)、UIDropInteraction(UIKit)和 View.setOnDragListener(Android)。
import { DatePicker } from "@perry/ui";

DatePicker(2026, 6, (iso) => {
  // iso is a POSIX-locale "yyyy-MM-dd" string
  console.log("picked", iso);
});

在这一窗口的更早些时候,widget 货架在桌面和移动端也填满了 — Combobox、TreeView、Calendar、Chart、CommandPalette、RichTextEditor、MapView、PdfView、BottomNavigation,以及一个可滑动的 ImageGallery — 每一个都由每个平台上真正的原生控件支撑。HarmonyOS(ArkTS)拿到了 Chart 和 TreeView(v0.5.893),这是它达到与其他平台等同所需的最后两个 widget。

GC、内部实现与稳定性

那 270 个版本里大多数不是头条 — 它们是 bug 修复和内部实现,而这正是本阶段的要点。有几个值得点名:

  • GC 继续推进。 GC 那篇文章里的有条件空闲列表工作继续稳定下来,并且关掉了一类尖锐的 bug:原生桥接的 Promise 现在在 tokio worker 上飞行期间被钉住,这样 GC 在 resolution 落地之前无法把它们 sweep 掉(v0.5.923)。如果你在负载下跑过一个 async fetch 并见到过一次幽灵回收,那就是这个。
  • 内存模型有了文档。 现在有一篇 internals/memory-model.md 深度剖析 — NaN-boxing、分代 GC、shadow stack 和 write barrier — 已接入文档站(v0.5.933)。
  • 一波由 npm 扫尾浮现的 codegen 稳定性修复:一个在恢复的 async step 内部被调用的模块级 const 箭头函数不再 SIGSEGV(v0.5.953),try { await rejected } catch { return X } 不再永久挂起(v0.5.870),还有真实 bundle 触发的少数 js_is_truthy / 裸指针范围崩溃。

Apple 杂务

较小但真实:perry setup ios --development 现在为开发构建做配置(v0.5.1023),而 Apple 的跨库 build/link 路径被去重并做成指针宽度可移植(v0.5.1121/1125)— 这正是为之前卡住的 npm / Homebrew / APT / winget 发布矩阵解阻的关键。

这把我们带到了哪里

Perry 背后的赌注一直是:“原生 TypeScript”只有在真实的 TypeScript 能跑时才有意义 — 不是一个玩具子集,而是人们真正 npm install 的那些包。这个月大多是这种工作:与其说是一个可以吹嘘的单一数字,不如说是一次漫长、不光鲜的推进,去填平“能编译”和“能工作”之间的差距。一致性雷达和 npm 等同性测试是我们现在盯着的记分牌,我们会继续发布这些数字 — 好的和仍不完美的。

源码:github.com/PerryTS/perry — Issues:github.com/PerryTS/perry/issues

— Ralph

喜欢这篇文章?获取下一篇。

关于 Perry 发布和我们下一步开发内容的简短笔记。

每月几封邮件。随时取消订阅。