从 Next.js 到 TanStack Start:两份 package.json 的差异
先把状态说清楚,否则底下所有对比都会被误读。
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-classic 的 apps/web/package.json 有 60 个 dependencies。按包名能确认用途的部分大致是这几组:
| 用途 | 依赖 |
|---|---|
| 框架 | next、react、react-dom |
| 状态 | zustand、use-deep-compare-effect |
| UI | radix-ui 及多个 @radix-ui/react-*、cmdk、sonner、vaul、lucide-react、react-icons、@hugeicons/*、embla-carousel-react、react-resizable-panels、react-window、@hello-pangea/dnd、motion |
| 媒体 | mediabunny、wavesurfer.js、soundtouchjs |
| Rust 核心 | opencut-wasm |
| 浏览器端 AI | @huggingface/transformers |
| 数据 | drizzle-orm、pg、postgres、@upstash/redis、@upstash/ratelimit |
| 认证与防护 | better-auth、botid |
| 部署 | @opennextjs/cloudflare、wrangler(dev) |
| 内容 | @content-collections/*(dev)、react-markdown、unified、rehype-*、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 install、bun 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-plugin,deploy 脚本是 bun run build && wrangler deploy) |
| React | 19.2 |
| 样式 | Tailwind CSS 4.1(@tailwindcss/vite)、tw-animate-css、tailwind-merge |
| UI | @base-ui/react、radix-ui、shadcn、cmdk、lucide-react、@hugeicons/*、vaul、sonner、input-otp、react-day-picker、react-resizable-panels、embla-carousel-react |
| 表单/校验 | react-hook-form、@hookform/resolvers、zod 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-ui、cmdk、lucide-react、@hugeicons/*、vaul、sonner、react-resizable-panels、embla-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'],dev 带 runInCI: 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.ts、wrangler.jsonc、moon.yml、package.json,是一个 Cloudflare Worker。我们没有读 index.ts,不知道它提供什么接口。
Rust 核心搬家了
这是两份文件之外、但必须一起看的一处差异。
classic 的 rust/crates/ 下有六个 crate:bridge、compositor、effects、gpu、masks、time,另有 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.rs、shell.rs、theme.rs、7 个 components/ 和 4 个 panels/(browser.rs、inspector.rs、preview.rs、timeline.rs)。这四个文件名看起来像一个编辑器的四区布局,但这是从文件名推断的,我们没有见过界面。顺带说一句,classic 仓库里也有一个 apps/desktop/,README 标注 in progress,同样是 GPUI 路线——两边同名不同仓,引用时别混。
这套读法你可以拿去用
抛开 OpenCut,从两份 package.json 里能提炼出几条判断依据,遇到任何陌生仓库都能套:
- 先数依赖分组,而不是数依赖个数。 有没有数据库客户端、有没有认证库、有没有限流,直接决定这是纯前端还是自带服务端——这一条比 README 的任何自我介绍都准。
- 看
scripts里 dev 和 deploy 那两行实际调了谁。vite dev --port 5173和bun run build && wrangler deploy这类命令是硬事实,端口、构建器、部署工具全在里面。 - 看有没有把自家的 Rust/WASM 产物当依赖引进来。 classic 那行
opencut-wasm说明跨语言的链路已经接通;反过来,如果Cargo.toml里对应的成员还被注释着,说明那条路还没铺完。 - 看有没有测试相关依赖,以及跑测试的任务定义在哪。 重写版的
apps/web/moon.yml里test任务写了inputs: ['src/**/*', 'tsconfig.json'],这类inputs声明决定了缓存与增量的粒度。 - 看工具链版本钉在哪。 一个仓库把 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 Actions 系统:快捷键、按钮、右键菜单的统一触发层
- 从 0.1.0 到 0.3.0:官方 changelog 记了些什么
- 重写版的栈:TanStack Start、Vite、Cloudflare 与 Rust gpui
本文描述的是 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.json、Cargo.toml 与 changelog/ 整理,核对日 2026-08-09。本文内容为仓库源码与文档口径,我们没有编译或运行过任何版本的 OpenCut。许可条款请以官方 LICENSE 原文为准,本文不构成法律意见。