Friendship ended with Deno, now Node is my best friend

Friendship ended with Deno, now Node is my best friend

Friendship ended with Deno, now Node is my best friend Saturday 3 Oct 2026

It’s finally time I go crawling back to Node! I’ve been using Node heavily this month on a SvelteKit client project. When did Node get so good‽ Deno has been my go-to runtime for so long I forgot how to Node. Now I’m back, I find all the ECMAScript† sugar is supported and the old annoying APIs have been replaced or modernised. Most importantly, I never have to see require().

终于到了我不得不灰溜溜地回到 Node 的时候了!这个月我在一个 SvelteKit 客户项目中大量使用了 Node。Node 是什么时候变得这么好用了?Deno 作为我的首选运行时已经太久了,以至于我都忘了怎么用 Node。现在我回来了,发现所有的 ECMAScript† 语法糖都得到了支持,那些陈旧且令人讨厌的 API 也已被替换或现代化。最重要的是,我再也不用看到 require() 了。

† Doesn’t seem like that Oracle trademark dispute will see a positive end :( † 看起来那场关于 Oracle 的商标纠纷似乎不会有圆满的结局 :(

Package management

包管理

The official Node docs recommend piping an internet script straight to bash (we never learn) to install NVM to manage Node & NPM. My (ancient) experience with NVM and NPM hasn’t been stellar. I heard Fast Node Manager (FNM) was better to switch Node versions. Obviously I roll bleeding-edge but I have client projects that demand stability.

Node 官方文档建议直接将网络脚本通过管道传输到 bash(我们永远记不住教训)来安装 NVM 以管理 Node 和 NPM。我(陈旧的)NVM 和 NPM 使用体验并不算好。我听说 Fast Node Manager (FNM) 在切换 Node 版本方面更好。显然我喜欢追求前沿技术,但我手头有需要稳定性的客户项目。

I opted for PNPM too to avoid getting immediately pwned. (The “M” in NPM stands for “malware.”) Some scripts I use have hard-coded binary names‡, so I added two aliases:

alias npm=pnpm
alias npx=pnpx

Maybe that’s a crime but so far it’s worked flawlessly.

我也选择了 PNPM 以免立刻被“黑”(NPM 中的“M”代表“恶意软件”)。我使用的一些脚本有硬编码的二进制名称‡,所以我添加了两个别名:

alias npm=pnpm
alias npx=pnpx

这可能是一种“罪行”,但到目前为止它运行得非常完美。

‡ Edit: I might be confusing the time I symlinked various binaries. The aliases do help when copying steps from install guides that assume NPM is installed. ‡ 编辑:我可能记混了当初软链接各种二进制文件的时候。这些别名在复制那些假设已安装 NPM 的安装指南步骤时确实很有帮助。

PNPM also blocks post-install scripts. Does NPM still yolo those? I added additional settings to pnpm-workspace.yaml to delay malware updates.

minimumReleaseAge: 1440
trustPolicy: no-downgrade

At first I tried setting the minimum release age to “one month” because it takes Microsoft at least that long to remove reported malware. This caused dependency issues where PNPM struggled to match suitable versions. I settled for “one day”; long enough to allow some other sucker to beta test the next Shai‑Hulud.

PNPM 还会拦截安装后脚本。NPM 现在还在盲目执行这些吗?我在 pnpm-workspace.yaml 中添加了额外设置来延迟恶意软件更新。

minimumReleaseAge: 1440
trustPolicy: no-downgrade

起初我尝试将最小发布年龄设置为“一个月”,因为微软至少需要那么久才能移除已报告的恶意软件。但这导致了依赖问题,PNPM 难以匹配到合适的版本。我最终定为“一天”;这足够让其他倒霉蛋去为下一个“沙虫”(Shai-Hulud)做内测了。

TypeScript

TypeScript

Node can now run TypeScript without throwing a tantrum like a baby if the stars don’t align. That said, one does not simply publish TypeScript packages to NPM.

error: [ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING]: Stripping types is currently unsupported for files under node_modules

Why? Just strip the types bro, I know you can! Let me sign a deal with the devil!

Node 现在可以运行 TypeScript 了,而不会像个婴儿一样因为星象不对就发脾气。话虽如此,发布 TypeScript 包到 NPM 并非易事。

error: [ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING]: Stripping types is currently unsupported for files under node_modules

为什么?直接剥离类型就行了兄弟,我知道你能做到!让我和魔鬼签个契约吧!

To discourage package authors from publishing packages written in TypeScript, Node.js refuses to handle TypeScript files inside folders under a node_modules path. — Node.js v26.10.0 documentation

为了劝阻包作者发布用 TypeScript 编写的包,Node.js 拒绝处理 node_modules 路径下文件夹内的 TypeScript 文件。 —— Node.js v26.10.0 文档

This restriction is philosophical rather than technical. I get it though. TypeScript is a Microsoft product. Opening that floodgate would pollute the entire ecosystem. Nobody wants more Microsoft. I’d love to see light “native types” in ECMAScript. There are type annotation proposals. I suspect I’ll be retired before those bear fruit.

这种限制更多是哲学上的而非技术上的。但我理解。TypeScript 是微软的产品。打开那个闸门会污染整个生态系统。没人想要更多的微软。我希望能看到 ECMAScript 中轻量级的“原生类型”。目前有一些类型注解提案。我怀疑在它们开花结果之前我就已经退休了。

No TypeScript packages mean I need to find the latest churnware slop to bundle my stuff. Tsdown did the trick, with only two additional dotfiles. Not thrilled about that (every dotfile represents a mistake). I suppose I break even after deleting deno.json etc.

没有 TypeScript 包意味着我得找最新的“折腾软件”来打包我的东西。Tsdown 搞定了,只多了两个点文件。对此我并不兴奋(每一个点文件都代表一个错误)。我想在删掉 deno.json 等文件后,我算是扯平了。

Speaking of Microsoft lock-in, because they wrecked GitHub I’m self-hosting my own Forgejo instance. NPM limitations mean my packages have lost “provenance”. I had to configure the PNPM trust policy to allow my own stuff. Fun times!

说到微软的锁定,因为他们搞砸了 GitHub,我正在自托管自己的 Forgejo 实例。NPM 的限制意味着我的包失去了“来源证明”(provenance)。我不得不配置 PNPM 的信任策略来允许我自己的东西。真是有趣的时光!

Migrating my website

迁移我的网站

My final test for Node was converting my static site generator from Deno. Not many Node versions ago this would have required a major refactor. Today with Node v26.10.0 I found surprisingly little work to do.

我对 Node 的最终测试是将我的静态网站生成器从 Deno 转换过来。在不久前的 Node 版本中,这需要进行大规模重构。而今天在 Node v26.10.0 中,我发现需要做的工作出奇地少。

The only required changes were to replace Deno’s file system API with node:fs — which is vastly improved from what I remember (literally ~10 years ago). Aside from that, I had to replace Deno.serve with Hono’s node adapter (a wrapper around node:http).

唯一需要的更改是将 Deno 的文件系统 API 替换为 node:fs——与我记忆中(大约 10 年前)相比,它有了巨大的改进。除此之外,我不得不将 Deno.serve 替换为 Hono 的 node 适配器(一个围绕 node:http 的包装器)。

After this minimum-viable migration I was shocked to see 15% faster builds. My codebase still favours idiomatic Deno. I bet I’m leaving performance on the table by not using other built-in Node APIs. That’s something to explore later. The only further change I made was to replace Deno’s @std/path with node:path which is a straight import swap.

在完成这次最小可行性迁移后,我震惊地发现构建速度快了 15%。我的代码库仍然偏向于 Deno 的惯用写法。我敢打赌,因为没有使用其他内置的 Node API,我浪费了性能。这是以后需要探索的事情。我做的唯一进一步更改是将 Deno 的 @std/path 替换为 node:path,这只是简单的导入替换。

So if I were to TL;DR in the middle: Node got a glow-up, wow!

所以如果我要在中间总结一下:Node 变强了,哇!

You’ve probably known this for a while. I kept using Deno out of habit and familiarity. And I haven’t exactly been enthused to write server-side JavaScript recently.

你可能早就知道这一点了。我一直使用 Deno 是出于习惯和熟悉。而且我最近对编写服务器端 JavaScript 并没有太大的热情。

Deno’s decline

Deno 的衰落

I’m burying this part because it’s flogging a dead horse. Ultimately, Deno failed when they allowed the Silicon Valley Circus to define “success”. Deno went from an innovative modern JavaScript runtime to a boring start-up with uncompelling products. Half the employees were laid off and what’s left are tweeting AI fantasies and vibe-coding Temu Cloudflare.

我把这部分放在最后,因为这已经是鞭尸了。归根结底,Deno 的失败在于他们允许硅谷马戏团来定义“成功”。Deno 从一个创新的现代 JavaScript 运行时变成了一个产品平庸的无聊初创公司。一半员工被裁掉,剩下的人在推特上发着 AI 幻想,并进行着 Temu 版 Cloudflare 的“氛围编程”。

There is no reason to use the Deno runtime today. Deno Land Inc. stopped innovating that years ago. Node has slowly but surely caught up, even surpassing Deno in places.

今天没有任何理由使用 Deno 运行时。Deno Land Inc. 多年前就停止了对其的创新。Node 缓慢但坚定地追了上来,甚至在某些地方超越了 Deno。

What finally pushed me away was:

  • Broken ZSH integration for weeks
  • JSR’s aggressive “429 (Too Many Requests)”
  • Bug(s) that made Deno choke on concurrent HTTP requests

最终让我离开的原因是:

  • 长达数周的 ZSH 集成损坏
  • JSR 激进的“429(请求过多)”限制
  • 导致 Deno 在处理并发 HTTP 请求时卡死的 Bug

Basically stuff that made it borderline unusable on top of my other criticism. JSR support were very quick to delete my account on request. I don’t like leaving dead profiles around the internet. None of my packages are visible but old versions remain installable.

基本上,除了我其他的批评之外,这些问题让它几乎无法使用。JSR 客服在收到请求后非常迅速地删除了我的账户。我不喜欢在互联网上留下死掉的个人资料。我的包现在都不可见了,但旧版本仍然可以安装。

It was fun early on but now it’s time to say goodbye.

brew uninstall deno

早期它很有趣,但现在是时候说再见了。

brew uninstall deno