从 Next.js 到 TanStack Start:两份 package.json 的差异

2026-08-09

先把状态说清楚,否则底下所有对比都会被误读。

OpenCut 现在有两个仓库,状态完全不同。旧版在 OpenCut-app/opencut-classic,README 标题写的是 “OpenCut (Legacy)“,紧跟着的第一句就是 “This is the original OpenCut codebase. It’s archived and no longer maintained.”——已归档、不再维护,最后一次推送是 2026-05-17。重写版在 OpenCut-app/OpenCut,README 的 Status 段明写 “OpenCut is being rewritten from the ground up.”,并且紧接着说旧版本仍在 classic 仓库、opencut.app 线上跑的仍然是 classic 版本,重写版会先住在 new.opencut.app,直到它准备好接管。

所以这篇文章拿来对照的两份 package.json,一份属于一个已经停更的代码库,一份属于一个尚未发布的代码库。两边都不是”当前版本的演进”,这一点先记住。

为什么值得单独看 package.json

读一个陌生项目,README 是项目方想让你看到的样子,package.json 是他们实际装了什么。前者会写愿景,后者只会写事实:依赖列表里出现了什么包、scripts 里那行命令实际调用的是谁、构建产物往哪个目录写。这份文件不会替项目美化进度。

OpenCut 这个案例特别适合练这个读法,因为两份文件隔着一次从头重写,差异是成块出现的,不是零敲碎打换个 UI 库。

classic 那份:一个完整的 Next.js 应用

opencut-classicapps/web/package.json 有 60 个 dependencies。按包名能确认用途的部分大致是这几组:

用途依赖
框架nextreactreact-dom
状态zustanduse-deep-compare-effect
UIradix-ui 及多个 @radix-ui/react-*cmdksonnervaullucide-reactreact-icons@hugeicons/*embla-carousel-reactreact-resizable-panelsreact-window@hello-pangea/dndmotion
媒体mediabunnywavesurfer.jssoundtouchjs
Rust 核心opencut-wasm
浏览器端 AI@huggingface/transformers
数据drizzle-ormpgpostgres@upstash/redis@upstash/ratelimit
认证与防护better-authbotid
部署@opennextjs/cloudflarewrangler(dev)
内容@content-collections/*(dev)、react-markdownunifiedrehype-*feed

这张表里最能说明架构取向的是三处。第一是 opencut-wasm——它是 classic 仓库 rust/wasm/ 发布出来的 npm 包,也就是说 Rust 编译出的 WASM 核心是被 web 应用当成普通依赖引进来的。第二是 mediabunny 这类媒体处理库和 @huggingface/transformers 同时在列,方向上指向”重活尽量在浏览器本地做”。第三是数据、认证、限流那一整组:drizzle-orm + postgres + @upstash/redis + better-auth,这不是一个纯前端项目的依赖构成,它自带服务端。

对应的运行方式也就好理解了。classic 的 README 给的本地步骤是先复制环境变量文件,再 docker compose up -d db redis serverless-redis-http 把数据库和 Redis 起来,然后 bun installbun dev:web,应用跑在 http://localhost:3000。README 注明 Docker 是可选但推荐的,只做前端可以跳过。

需要提醒一句:依赖清单只能推断用途方向。看到 wavesurfer.js 只能说”依赖里包含音频波形库”,不能说它在界面上画了什么;我们没有跑过 bun dev:web,也没有打开过编辑器。

重写版那份:TanStack Start + Vite

重写主仓 apps/web/package.json 的选型和上面基本不是同一套:

方面重写版
路由/框架TanStack Router + TanStack Start(@tanstack/react-router@tanstack/react-start@tanstack/react-router-ssr-query@tanstack/router-plugin
构建Vite(vite dev --port 5173@vitejs/plugin-react
部署Cloudflare(@cloudflare/vite-plugindeploy 脚本是 bun run build && wrangler deploy
React19.2
样式Tailwind CSS 4.1(@tailwindcss/vite)、tw-animate-csstailwind-merge
UI@base-ui/reactradix-uishadcncmdklucide-react@hugeicons/*vaulsonnerinput-otpreact-day-pickerreact-resizable-panelsembla-carousel-react
表单/校验react-hook-form@hookform/resolverszod 4.4
图表recharts 3.8
测试Vitest + @testing-library/react@testing-library/dom

把两张表叠起来看,真正换掉的是三条线:

框架线next 换成 TanStack Router + TanStack Start。 构建线:Next 自带的构建换成 Vite,dev 命令直接写着 vite dev --port 5173部署线@opennextjs/cloudflare 换成 @cloudflare/vite-plugin,收尾从”Next 产物适配到 Cloudflare”变成 bun run build && wrangler deploy

没换的那条线也值得看:UI 层。radix-uicmdklucide-react@hugeicons/*vaulsonnerreact-resizable-panelsembla-carousel-react 两边都在。也就是说这次重写换的是框架与构建基座,组件层的口味基本平移过来了。至于他们为什么这么选,README 没有给理由,我们也不猜。

另一处差异是测试:重写版列了 Vitest 加 Testing Library,classic 那份 60 个 dependencies 的分组里我们没有看到对应的测试组。这只是两份清单的差异陈述,不构成对任一方工程质量的判断。

还有一个位置容易被略过:任务是怎么被组织起来的。重写版的 apps/web/moon.yml 把 dev、build、test、deploy 四个任务写成了显式定义,其中 build 声明了 inputs: ['src/**/*', 'public/**/*', 'vite.config.ts', 'tsconfig.json', 'package.json']outputs: ['dist']deploy 则写着 deps: ['~:build']devrunInCI: false。这几行的含义是:改动落在 inputs 之外的文件不会让 build 失效,产物目录被声明出来才能被缓存复用,而部署前必须先构建这件事是写在配置里的,不是靠人记得。classic 那边的启动方式是直接一条 bun dev:web,两种组织粒度不在一个层面上。

连启动方式一起变了

栈换了,进项目的第一条命令也换了。重写版不是 bun install 起步,README 要求先装 proto:

# Linux, macOS, WSL
bash <(curl -fsSL https://moonrepo.dev/install/proto.sh)
# Windows PowerShell
irm https://moonrepo.dev/install/proto.ps1 | iex

Windows 上如果 shim 跑不起来,README 给的处置是 Set-ExecutionPolicy -Scope CurrentUser RemoteSigned,为当前用户放开本地脚本。

然后从仓库根:

proto use              # 安装 .prototools 里钉住的工具

moon run web:dev       # localhost:5173
moon run api:dev       # localhost:8787
moon run desktop:dev   # 见 apps/desktop/README.md

.prototools 全文就三行钉版本,外加一句注释说明意图:

# proto pins tool versions workspace-wide.
# Every developer and CI machine gets the exact same versions automatically.
moon = "2.3.3"
bun  = "1.3.11"
rust = "1.97.0"

这三个端口值得记一下:classic 是 3000,重写版 web 是 5173、api 是 8787。8787 是 wrangler dev 的默认端口,apps/api/ 在重写主仓里只有四个文件——src/index.tswrangler.jsoncmoon.ymlpackage.json,是一个 Cloudflare Worker。我们没有读 index.ts,不知道它提供什么接口。

Rust 核心搬家了

这是两份文件之外、但必须一起看的一处差异。

classic 的 rust/crates/ 下有六个 crate:bridgecompositoreffectsgpumaskstime,另有 rust/wasm/,发布为 npm 包 opencut-wasm——正好对应 web 那份 package.json 里的那一行依赖。也就是说在 classic 里,Rust 核心是通过 WASM 进到浏览器的,路径是通的。

重写主仓的根 Cargo.toml 是这样:

[workspace]
resolver = "3"
members = [
  'apps/desktop',
  # 'crates/*',
]

[workspace.package]
version = "0.1.0"
edition = "2024"
license = "MIT"

[workspace.dependencies]
gpui = "0.2.2"

workspace 版本 0.1.0、edition 2024、GUI 框架是 gpui 0.2.2,成员只有 apps/desktop 一个,crates/* 那一行被注释掉了。README 承诺的”Rust 核心”在这个仓库里还没有对应的 crate。这只是陈述当前代码结构,不做进度评价,也不预测时间表。

重写主仓里那个唯一的 workspace 成员,apps/desktop/src/ 目前是 main.rsshell.rstheme.rs、7 个 components/ 和 4 个 panels/browser.rsinspector.rspreview.rstimeline.rs)。这四个文件名看起来像一个编辑器的四区布局,但这是从文件名推断的,我们没有见过界面。顺带说一句,classic 仓库里也有一个 apps/desktop/,README 标注 in progress,同样是 GPUI 路线——两边同名不同仓,引用时别混。

这套读法你可以拿去用

抛开 OpenCut,从两份 package.json 里能提炼出几条判断依据,遇到任何陌生仓库都能套:

  1. 先数依赖分组,而不是数依赖个数。 有没有数据库客户端、有没有认证库、有没有限流,直接决定这是纯前端还是自带服务端——这一条比 README 的任何自我介绍都准。
  2. scripts 里 dev 和 deploy 那两行实际调了谁。 vite dev --port 5173bun run build && wrangler deploy 这类命令是硬事实,端口、构建器、部署工具全在里面。
  3. 看有没有把自家的 Rust/WASM 产物当依赖引进来。 classic 那行 opencut-wasm 说明跨语言的链路已经接通;反过来,如果 Cargo.toml 里对应的成员还被注释着,说明那条路还没铺完。
  4. 看有没有测试相关依赖,以及跑测试的任务定义在哪。 重写版的 apps/web/moon.ymltest 任务写了 inputs: ['src/**/*', 'tsconfig.json'],这类 inputs 声明决定了缓存与增量的粒度。
  5. 看工具链版本钉在哪。 一个仓库把 moon、bun、rust 三个版本写进 .prototools,和只在 README 里写一句”建议 Node 20+“,是两种协作强度。

同时要清楚这套读法的边界:package.json 只能告诉你装了什么,不能告诉你用得怎么样。看到 @huggingface/transformers 只能推出”方向上在浏览器里跑模型”,推不出跑得动跑不动;看到路由框架换了,也推不出任何性能结论。判断能力必须回到代码或官方文档,不能停在依赖清单上。

另外,重写主仓 README 明写 “We’re not set up to take outside contributions yet while the architecture is being designed.”——架构还在设计中,暂不接受外部贡献。如果你读完想去提 PR,先看这句话;想跟进的话,README 给的是 Discord(discord.gg/zmR9N35cjK)和开 issue 两条路。

两个仓库的许可都标注 MIT,但我们一份 LICENSE 正文都没读过,商用边界一类问题请以官方 LICENSE 原文为准。

延伸阅读


本文描述的是 OpenCut 旧版(classic)的行为。该代码库已归档、不再维护,opencut.app 线上目前仍运行该版本;重写版正在开发中,功能与操作可能变化。

本文描述的是 OpenCut 重写版仓库当前的代码结构与官方 README 声明的路线图。该版本尚未发布,Editor API、插件体系、MCP server、无头模式等均为官方声明的计划,我们没有见到可用实现。

本文依据 OpenCut 官方仓库(github.com/OpenCut-app/OpenCut 与已归档的 github.com/OpenCut-app/opencut-classic)的 README、docs/ 架构文档、package.jsonCargo.tomlchangelog/ 整理,核对日 2026-08-09。本文内容为仓库源码与文档口径,我们没有编译或运行过任何版本的 OpenCut。许可条款请以官方 LICENSE 原文为准,本文不构成法律意见。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。