-
Notifications
You must be signed in to change notification settings - Fork 188
Migrate @workflow/web from Next.js to React Router v7
#1005
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
🦋 Changeset detectedLatest commit: ca37040 The changes in this PR will be included in the next version bump. This PR includes changesets to release 15 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
🧪 E2E Test Results❌ Some tests failed Summary
❌ Failed Tests🌍 Community Worlds (41 failed)turso (41 failed):
Details by Category✅ ▲ Vercel Production
✅ 💻 Local Development
✅ 📦 Local Production
✅ 🐘 Local Postgres
✅ 🪟 Windows
❌ 🌍 Community Worlds
✅ 📋 Other
|
📊 Benchmark Results
workflow with no steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Express | Nitro workflow with 1 step💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) workflow with 10 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) workflow with 25 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) | Express workflow with 50 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) | Express Promise.all with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro | Express Promise.all with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) Promise.all with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) Promise.race with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Express | Nitro Promise.race with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Nitro | Next.js (Turbopack) Promise.race with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) Stream Benchmarks (includes TTFB metrics)workflow with stream💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) SummaryFastest Framework by WorldWinner determined by most benchmark wins
Fastest World by FrameworkWinner determined by most benchmark wins
Column Definitions
Worlds:
|
This stack of pull requests is managed by Graphite. Learn more about stacking. |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
@workflow/web from Next.js to React Router v7
Only console.error for genuine server-side failures (5xx). API-level client errors (4xx), run-not-found errors, and unrecognized errors from world backends are not logged server-side — the error is returned to the caller for handling.
Move the stray-POST handling from individual route actions to a single catch-all action on root.tsx. This covers all current and future routes without needing per-route action stubs.
Switch the RPC layer from JSON to CBOR encoding to preserve binary data (Uint8Array) across the wire. CBOR natively handles binary types without base64 overhead. Hydration (deserializing input/output/eventData from binary format to JS values) stays server-side for now. Making @workflow/core's hydration code browser-safe requires extracting it from the serialization module which has deep Node.js dependencies (Buffer, AsyncLocalStorage, child_process, etc.). This will be addressed as a prerequisite when e2e encryption lands. - Add cbor-x as runtime dependency - RPC route: encode responses as CBOR - RPC client: send/receive CBOR with proper Content-Type headers - Server actions: hydrate data before CBOR encoding (no JSON round-trip needed since CBOR preserves all JS types) - Hydration errors are caught and return raw data instead of crashing
- Add error handling for malformed CBOR/JSON request bodies (400) - Remove unused readStreamServerAction import from RPC route - Validate streamId format and startIndex parameter in stream route - Decode CBOR error body in RPC client for better error messages - Remove duplicate static file middleware from server/app.ts - Fix dev script to use react-router dev instead of node server.js
Vite's SSR build externalizes certain packages instead of bundling them. When @workflow/web is installed from npm (not in the monorepo), these externalized packages must be in dependencies to be available at runtime. Move all Radix UI, lucide-react, class-variance-authority, clsx, tailwind-merge, date-fns, next-themes, sonner, @xyflow/react, and workspace packages to dependencies. Add react/react-dom as peerDependencies.
Use ssr.noExternal=true in Vite config to bundle all dependencies into the server build. This means @workflow/web only needs express at runtime — all UI dependencies (Radix, lucide-react, etc.) are compiled into the server bundle. Keeps the installed package small.
e89223f to
ca37040
Compare
…transport Move observability hydration to the shared serialization-format + web/cli-specific hydration modules, shift web UI hydration to the client side, and align RPC/CBOR behavior to reduce merge conflict risk with PR #1005/#1015 while fixing binary payload rendering in observability views. Co-authored-by: Cursor <[email protected]>

Summary
nextdependency from the web, CLI, and workflow metapackagesnext startas a child processnuqsURL state management with React Router'suseSearchParams/api/rpc) and a thin CBOR-based clientMotivation
The
nextpackage is ~300MB installed and was the single largest dependency in the monorepo. It also required spawning a separate child process from the CLI to run the o11y web server, adding complexity around process lifecycle management, port readiness polling, and environment variable forwarding.With React Router framework mode, the web package builds to a standard Express-compatible server bundle that the CLI can import and serve directly in its own process.
What changed
Framework swap (
@workflow/web):next.config.ts/postcss.config.mjs→react-router.config.ts/vite.config.tssrc/directory →app/directory (React Router convention)src/app/layout.tsx+layout-client.tsx→app/root.tsxsrc/app/page.tsx→app/routes/home.tsxsrc/app/run/[runId]/page.tsx→app/routes/run-detail.tsx@/→~/'use client'/'use server'directivesData transport:
/api/rpcwith CBOR encodingUint8Arrayand other binary types natively (no base64 overhead)/api/stream/:streamIdresource routeURL state:
nuqs(useQueryState) →useSearchParamsfromreact-routerFonts:
next/font/google→ Geist.woff2files referenced directly fromnode_modules/geistvia@font-facein CSSCLI integration (
@workflow/cli):import('@workflow/web/server').then(m => m.startServer(port))Radix UI compatibility:
onSubmitpreventDefault onAlertDialogContentandSheetContentto prevent Radix's internal<form method="dialog">from triggering React Router route actionsDependencies removed
next,swr,nuqs,@tailwindcss/postcssDependencies added
react-router/@react-router/dev/@react-router/node/@react-router/express(all7.13.0)express,vite,@tailwindcss/vite,cbor-x,isbot,cross-envgeist(devDep)