Skip to content

Commit 0e21ced

Browse files
committed
feat(ud): Phase 2, 3 and 4 (#1663)
1 parent fb6cb96 commit 0e21ced

22 files changed

Lines changed: 3013 additions & 224 deletions

docs/implementation-docs/2026-03-26-cedarjs-project-overview.md

Lines changed: 59 additions & 52 deletions
Large diffs are not rendered by default.

docs/implementation-plans/universal-deploy-integration-plan-refined.md

Lines changed: 314 additions & 110 deletions
Large diffs are not rendered by default.

docs/implementation-plans/universal-deploy-phase-4-detailed-plan.md

Lines changed: 1196 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
# Detailed Plan: Universal Deploy Phase 5 — Idiomatic Vite Full-Stack Integration
2+
3+
## Summary
4+
5+
Phase 4 delivered a working Vite-centric full-stack runtime: one `cedar dev`
6+
command, API HMR through Vite's SSR module graph, and a Vite-built UD Node
7+
server entry for `cedar serve`. However, the underlying architecture is an
8+
**incremental bridge** that still deviates from what the Vite team recommends
9+
for full-stack frameworks.
10+
11+
Phase 5 closes that architectural gap by making Cedar's Vite integration
12+
**idiomatic**:
13+
14+
- **Dev**: one Vite dev server with a single visible port and API middleware
15+
inline — no separate HTTP listener for the API side
16+
- **Build**: Vite's `buildApp()` API (or the builder `buildApp()` hook) used to
17+
build the **client** and **api** environments together in a single build pass,
18+
with environments declared in `vite.config`
19+
20+
This is foundational infrastructure, not a user-facing feature. It makes later
21+
phases (per-route UD registration, SSR rebuild) simpler and more robust by
22+
ensuring Cedar's Vite integration follows the same patterns as the rest of the
23+
Vite full-stack ecosystem.
24+
25+
## Why Phase 5 Exists
26+
27+
Phase 4 took the shortest path to user-facing wins. It runs two HTTP listeners
28+
in dev (web Vite server + API Vite SSR + Fastify) and uses three separate
29+
`viteBuild()` calls in production. That was the right trade-off for Phase 4,
30+
but it leaves technical debt that compounds if not addressed before the next
31+
major milestones.
32+
33+
### Two problems to solve
34+
35+
**1. Two-listener dev model**
36+
37+
`cedar-unified-dev` starts:
38+
39+
- a Vite client dev server on `webPort`
40+
- a Vite SSR dev server (`middlewareMode: true`) + Fastify on `apiPort`
41+
42+
The browser still conceptually targets two origins. Auth flows, CORS, and
43+
cookie handling are more complex than they need to be because the API is not
44+
served from the same origin as the web assets.
45+
46+
**2. Fragmented build pipeline**
47+
48+
`buildApiWithVite()`, `buildUDApiServer()`, and the web client build each call
49+
`viteBuild()` standalone. There are three independent Vite builds with no shared
50+
module graph, no shared transform pipeline, and no coordinated invalidation.
51+
52+
## Goals
53+
54+
### Primary Goals
55+
56+
- Replace the two-listener dev model with a **single Vite dev server** that
57+
handles both web and API requests on one visible port
58+
- Reimplement API request handling as **Vite middleware** (via
59+
`configureServer` hook or equivalent) rather than a separate Fastify listener
60+
- Adopt **`buildApp()` with declared environments** for production builds,
61+
replacing standalone `viteBuild()` calls for each side
62+
- Ensure the custom Fastify compatibility lane (Lane B) is **not affected** by
63+
these changes
64+
65+
### Secondary Goals
66+
67+
- Preserve all existing Phase 4 dev behavior: HMR, GraphQL, auth, functions,
68+
GraphiQL, direct `curl`
69+
- Maintain backward compatibility for the `cedar dev` CLI contract
70+
- Keep build output paths stable so `cedar serve` continues to work unchanged
71+
72+
## Non-Goals
73+
74+
- Adding new user-facing features (this is an internal architecture phase)
75+
- Changing the Cedar handler contract or middleware model
76+
- Rebuilding SSR or RSC (that is Phase 7)
77+
- Formalizing per-route UD registration (that is Phase 6)
78+
- Removing the custom Fastify compatibility lane
79+
- Supporting arbitrary Fastify plugins in the default runtime path
80+
81+
## Workstreams
82+
83+
## Workstream 1: Single-Listener Dev Server
84+
85+
### Objective
86+
87+
Move API request handling from a separate Fastify listener into the Vite dev
88+
server's middleware pipeline.
89+
90+
### Current State
91+
92+
`apiDevServer.ts` creates a Vite SSR dev server (`middlewareMode: true`) and
93+
mounts a Fastify app on a separate port. `cedarDevDispatcherPlugin` exists in
94+
`@cedarjs/vite` but is not installed in the dev server. The Fastify app handles:
95+
96+
- body parsing (`fastify-raw-body`)
97+
- URL data extraction (`fastify-url-data`)
98+
- route matching to the `LAMBDA_FUNCTIONS` registry
99+
- GraphQL Yoga streaming via `createFetchRequestFromFastify`
100+
- content-type parsing for form data and multipart
101+
102+
### Target State
103+
104+
A single `createServer()` call in `cedar-unified-dev` that:
105+
106+
- starts one Vite dev server on the visible port
107+
- installs `cedarDevDispatcherPlugin` (already built in Phase 4) into the
108+
`configureServer` middleware pipeline
109+
- routes API requests to Cedar's aggregate fetch dispatcher directly
110+
- falls through to Vite's normal web handling for non-API requests
111+
112+
### Tasks
113+
114+
- install `cedarDevDispatcherPlugin` into the web Vite dev server's
115+
`configureServer` hook (the plugin was built in Phase 4 but not wired up)
116+
- replace Fastify routing with fetch-native request classification and dispatch
117+
- implement body parsing as a utility function (or use a WHATWG-compatible
118+
parser) rather than a Fastify plugin
119+
- mount GraphQL Yoga directly inside the middleware pipeline using its
120+
`handle(request, context)` method, which already expects a Fetch `Request`
121+
- preserve the `LAMBDA_FUNCTIONS` registry and HMR invalidation logic — only
122+
the HTTP transport layer changes
123+
- ensure request context enrichment (cookies, params, query, auth state) still
124+
flows correctly without Fastify's `req`/`reply` objects
125+
- preserve error surfacing: backend errors should still be visible in both
126+
terminal and HTTP response where appropriate
127+
128+
### Blockers to Resolve
129+
130+
- GraphQL Yoga's `handle()` method expects a Fetch `Request` and returns a
131+
Fetch `Response`. This is already nearly the target shape, but the current
132+
code wraps it in `getAsyncStoreInstance().run()` inside a Fastify handler.
133+
That AsyncLocalStorage context needs to be established in the middleware
134+
pipeline instead.
135+
- The `requestHandler` helper from `@cedarjs/api-server/requestHandlers` is
136+
currently coupled to Fastify `req`/`reply` objects. It may need a thin
137+
fetch-native wrapper, or the helper itself may need to be split into
138+
transport-agnostic and Fastify-specific variants.
139+
140+
### Deliverable
141+
142+
- `cedar dev` runs on a single visible port with no separate API listener
143+
- API requests (GraphQL, auth, functions) are handled inline via Vite middleware
144+
- Web requests (assets, HMR, SPA fallback) continue through Vite's normal path
145+
146+
## Workstream 2: `buildApp()` with Declared Environments
147+
148+
### Objective
149+
150+
Replace the three separate `viteBuild()` invocations with a single `buildApp()`
151+
call that declares `client` and `api` environments.
152+
153+
### Current State
154+
155+
Production build uses three standalone Vite builds:
156+
157+
1. `buildApiWithVite()` — builds API functions with `ssr: true` and
158+
`preserveModules: true`
159+
2. `buildUDApiServer()` — builds the UD Node server entry with the
160+
`cedarUniversalDeployPlugin` and `node()` plugin
161+
3. `cedar-vite-build` binary — builds the web client bundle
162+
163+
These share no module graph, no transform pipeline, and no invalidation.
164+
Alias resolution, Babel plugin ordering, and externalization logic can diverge
165+
silently.
166+
167+
### Target State
168+
169+
A unified Vite config that declares:
170+
171+
```ts
172+
// Simplified illustration
173+
export default defineConfig({
174+
environments: {
175+
client: {
176+
// web browser bundle (SPA assets)
177+
build: {
178+
outDir: 'web/dist',
179+
// ...
180+
},
181+
},
182+
api: {
183+
// server-side API entry (Cedar aggregate fetchable)
184+
build: {
185+
ssr: true,
186+
outDir: 'api/dist',
187+
// ...
188+
},
189+
},
190+
},
191+
})
192+
```
193+
194+
A single `buildApp()` call builds both environments from the same module graph.
195+
196+
### Tasks
197+
198+
- evaluate Vite's `buildApp()` API stability and feature completeness for Cedar's
199+
use case (check current Vite version support)
200+
- merge the three existing build configurations into one unified config with
201+
declared environments
202+
- ensure `node()` from `@universal-deploy/node/vite` works correctly within the
203+
`buildApp()` environment model
204+
- ensure the web client build's special requirements (cwd, PostCSS/Tailwind
205+
resolution, etc.) are preserved in the unified config
206+
- verify that output paths remain stable so `cedar serve` does not need changes
207+
- add the `api` environment to the Vite config used by `cedar build`
208+
209+
### Blockers to Resolve
210+
211+
- `buildApp()` may not be fully stable or documented in the Vite version Cedar
212+
pins. This needs investigation before committing to the migration.
213+
- The web client build currently runs via a separate binary
214+
(`cedar-vite-build`) with its own config file. That binary changes `cwd`
215+
to the web directory for PostCSS/Tailwind correctness. The unified build
216+
needs to preserve that behavior or find an alternative.
217+
218+
### Deliverable
219+
220+
- `cedar build` uses a single `buildApp()` invocation for both client and api
221+
environments
222+
- Output directories and artifacts remain compatible with `cedar serve`
223+
224+
## Suggested Sequencing
225+
226+
1. **Single-listener dev first** — this is the higher-impact change for daily
227+
developer experience and should be validated before layering `buildApp()` on
228+
top of it
229+
2. **`buildApp()` second** — the build consolidation is less user-visible and
230+
can be done in parallel with single-listener testing, but it should not be
231+
released before single-listener is stable
232+
233+
## Relationship to Other Phases
234+
235+
- **Phase 4**: this phase replaces the incremental bridge with the idiomatic
236+
architecture. Phase 4 must be stable before starting this work.
237+
- **Phase 6 (UD per-route registration)**: single-listener makes per-route
238+
dispatch simpler because there's only one request classification layer to
239+
reason about. The dispatcher that routes to per-route entries is the same
240+
middleware that currently routes to the aggregate entry.
241+
- **Phase 7 (SSR rebuild)**: `buildApp()` with declared environments is
242+
prerequisite for SSR because SSR will add a third environment (`ssr` for
243+
HTML streaming / RSC). The build infrastructure must already support
244+
multiple environments.
245+
246+
## Exit Criteria
247+
248+
- `cedar dev` runs a single Vite dev server on one visible port for the default
249+
runtime path
250+
- API requests are handled inline via Vite middleware, not by a separate
251+
Fastify listener
252+
- `cedar build` uses `buildApp()` with declared `client` and `api`
253+
environments in a single build pass
254+
- All existing Phase 4 functionality (HMR, GraphQL, auth, functions, GraphiQL,
255+
direct `curl`, `cedar serve`) continues to work
256+
- The custom Fastify compatibility lane is unaffected
257+
258+
## Risks
259+
260+
- `buildApp()` API may not be mature enough in the pinned Vite version
261+
- Moving body parsing and request enrichment out of Fastify may surface edge
262+
cases in auth providers or GraphQL Yoga plugins that currently depend on
263+
Fastify-specific request shapes
264+
- Single-listener dev may complicate debugging if API and web errors are
265+
interleaved in the same Vite server output
266+
- The web client build's `cwd` sensitivity (PostCSS/Tailwind) may resist
267+
merging into a unified config
268+
269+
## Deliverables
270+
271+
- refactored `cedar-unified-dev` using single Vite dev server with inline API
272+
middleware
273+
- refactored `cedar build` using `buildApp()` with `client` and `api`
274+
environments
275+
- updated documentation reflecting the single-port dev model and unified build

packages/api-server/package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,18 @@
7373
"types": "./dist/cjs/bothCLIConfigHandler.d.ts",
7474
"default": "./dist/cjs/bothCLIConfigHandler.js"
7575
},
76+
"./udDispatcher": {
77+
"import": {
78+
"types": "./dist/udDispatcher.d.ts",
79+
"default": "./dist/udDispatcher.js"
80+
}
81+
},
82+
"./udFetchable": {
83+
"import": {
84+
"types": "./dist/udFetchable.d.ts",
85+
"default": "./dist/udFetchable.js"
86+
}
87+
},
7688
"./watch": {
7789
"import": {
7890
"types": "./dist/watch.d.ts",
@@ -130,6 +142,8 @@
130142
"@cedarjs/web-server": "4.0.0",
131143
"@fastify/multipart": "9.4.0",
132144
"@fastify/url-data": "6.0.3",
145+
"@universal-deploy/node": "^0.1.6",
146+
"@universal-deploy/store": "^0.2.1",
133147
"ansis": "4.2.0",
134148
"chokidar": "3.6.0",
135149
"dotenv-defaults": "5.0.2",
@@ -140,7 +154,9 @@
140154
"picoquery": "2.5.0",
141155
"pretty-bytes": "5.6.0",
142156
"pretty-ms": "7.0.1",
157+
"rou3": "^0.8.1",
143158
"split2": "4.2.0",
159+
"srvx": "^0.11.9",
144160
"termi-link": "1.1.0",
145161
"yargs": "17.7.2"
146162
},

0 commit comments

Comments
 (0)