Skip to content

Commit e541071

Browse files
authored
feat(ud): cedar serve --ud (#1727)
1 parent c2b98a7 commit e541071

6 files changed

Lines changed: 566 additions & 29 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ Routes.tsx ← 4 routes added inside <Set wrap={ScaffoldLayout} title="Posts" ..
242242
| storybook | Vite Storybook. |
243243
| project-config | Read cedar.toml. getPaths/getConfig/findUp. |
244244
| internal | Re-exports project-config+babel-config. buildApi/buildApiWithVite/dev/generate. Route extraction. |
245-
| api-server | Fastify. Auto-discover Lambda functions. Mount GraphQL. Custom server.ts. Opt-in srvx/WinterTC path via `cedar serve api --ud`. |
245+
| api-server | Fastify. Auto-discover Lambda functions. Mount GraphQL. Custom server.ts. Opt-in srvx/WinterTC path via `cedar serve api --ud` or `cedar serve --ud`. |
246246
| web-server | Fastify for web side. Uses fastify-web adapter. |
247247
| fastify-web | Fastify plugin. Static files, SPA fallback, API proxy, prerender. |
248248
| babel-config | Presets/plugins for api+web. registerApiSideBabelHook. |
@@ -271,6 +271,6 @@ Routes.tsx ← 4 routes added inside <Set wrap={ScaffoldLayout} title="Posts" ..
271271
- Entry: `entry.client.tsx` (always). \*SSR/RSC: also `entry.server.tsx`
272272
- Routes in `Routes.tsx` as JSX (virtual, never rendered — Babel auto-loads pages)
273273
- Build: default = esbuild (api) + Vite (web); `--ud` = unified Vite (web + api with `build.ssr: true` + `preserveModules: true` + Babel plugin)
274-
- Server: API always Fastify; opt-in srvx/WinterTC via `cedar serve api --ud` (`buildUDApiServer` emits `api/dist/ud/index.js`). Web: Fastify (SPA). \*SSR/RSC: Web uses Express
274+
- Server: API always Fastify; opt-in srvx/WinterTC via `cedar serve api --ud` or `cedar serve --ud` (`buildUDApiServer` emits `api/dist/ud/index.js`). Web: Fastify (SPA). \*SSR/RSC: Web uses Express
275275
- Package mgr: Yarn 4 (+ experimental support for npm and pnpm); Framework: Yarn 4 + Nx (build orchestration).
276276
- Codegen: compile-time (Vite plugins) + on-demand (cedar-gen)
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
# Plan: `cedar serve --ud` — Serve Both Web and API Sides
2+
3+
**Date:** 2026-05-08
4+
**Status:** Draft
5+
**Relates to:** Universal Deploy integration plan (Phases 3–6), Phase 6 Addendum
6+
7+
---
8+
9+
## Problem Statement
10+
11+
Today, `cedar serve --ud` does not exist. The `--ud` flag is only wired to the
12+
`cedar serve api` sub-command. Running `cedar serve` (no side specified, the
13+
"both" case) always uses the legacy Fastify pair: a web Fastify server on
14+
`webPort` that proxies API calls to a separate API Fastify server on `apiPort`.
15+
16+
The goal is to support `cedar serve --ud` (no side qualifier) for
17+
**local production-like testing** — i.e. verifying a `cedar build --ud` output
18+
behaves correctly before deploying.
19+
20+
---
21+
22+
## Command Roles
23+
24+
It is worth being explicit about what each command is for:
25+
26+
| Command | Purpose |
27+
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------- |
28+
| `cedar serve api --ud` | **Production.** Runs `api/dist/ud/index.js` (srvx). Nginx, or some other reverse proxy, serves web assets separately. |
29+
| `cedar serve --ud` | **Local production-like testing.** Runs the UD API entry + a web static file server so the full app is usable in a browser. |
30+
| `cedar serve` (both) | **Legacy.** Fastify web (with proxy) + Fastify API. Unchanged. |
31+
32+
In real baremetal/VPS production the topology is:
33+
34+
- **nginx** serves `web/dist/` static files directly
35+
- **nginx** proxies API routes to a Node process (`api/dist/ud/index.js`)
36+
37+
`cedar serve --ud` mirrors this as closely as possible locally:
38+
39+
- **Fastify web server** (existing `redwoodFastifyWeb`) serves `web/dist/`
40+
and proxies API requests to the UD API process. Fastify is basically standing
41+
in for nginx
42+
- **Forked `api/dist/ud/index.js`** handles all API routes via srvx
43+
44+
No changes to the Vite plugin or the build pipeline are needed.
45+
46+
---
47+
48+
## Current State
49+
50+
| Command | What happens today |
51+
| ---------------------- | -------------------------------------------------- |
52+
| `cedar serve api` | Fastify API server |
53+
| `cedar serve api --ud` | Forks `api/dist/ud/index.js` (srvx) |
54+
| `cedar serve web` | Fastify web server (`@cedarjs/web-server`) |
55+
| `cedar serve` (both) | Fastify web (with proxy) + Fastify API |
56+
| `cedar serve --ud` | Not recognised / falls through to "both" (Fastify) |
57+
58+
---
59+
60+
## Implementation Plan
61+
62+
### Step 1 — Add `--ud` to the `$0` (both) sub-command
63+
64+
**Files changed:**
65+
66+
- `packages/cli/src/commands/serve.ts`
67+
68+
**What to do:**
69+
70+
1. Add `--ud` to the `$0` yargs sub-command's `builder`, mirroring the `api`
71+
sub-command:
72+
73+
```ts
74+
yargs.option('ud', {
75+
description:
76+
'Use the Universal Deploy server (srvx) for the API side. ' +
77+
'The web side is served by the existing static file server. ' +
78+
'Pass --ud to opt in; the default is Fastify for both sides.',
79+
type: 'boolean',
80+
default: false,
81+
})
82+
```
83+
84+
2. In the `$0` handler, when `argv.ud` is `true`:
85+
86+
a. **Validate build artifacts** — both must exist before starting:
87+
- `api/dist/ud/index.js` — if missing, print a clear error pointing to
88+
`yarn cedar build --ud` and exit.
89+
- `web/dist/index.html` — if missing, print a clear error pointing to
90+
`yarn cedar build` and exit.
91+
92+
b. **Resolve ports** — use the same helpers as the existing both-handler:
93+
- Web: `getWebHost()` / `getWebPort()` (default `8910`)
94+
- API: `getAPIHost()` / `getAPIPort()` (default `8911`)
95+
96+
c. **Start the Fastify web server** with `apiProxyTarget` pointing at the UD
97+
API port — exactly as the existing `bothCLIConfigHandler` does, just
98+
substituting the forked UD entry for the Fastify API server.
99+
100+
d. **Fork `api/dist/ud/index.js`** — same pattern as `cedar serve api --ud`,
101+
passing `--port` / `--host` for the API port/host.
102+
103+
e. **Print both addresses** once both are up.
104+
105+
3. When `argv.ud` is `false`, the existing Fastify-pair behaviour is completely
106+
unchanged.
107+
108+
---
109+
110+
### Step 2 — Update the build-artifact middleware check
111+
112+
The existing `serve.ts` middleware already validates that the relevant dist
113+
directories exist before any sub-command handler runs. Extend it so that when
114+
`--ud` is present in the default (both) case, it also checks for
115+
`api/dist/ud/index.js` and emits a helpful error pointing to
116+
`yarn cedar build --ud` rather than the generic `yarn cedar build`.
117+
118+
---
119+
120+
### Step 3 — Update error messages
121+
122+
The build-not-found error in the `--ud` path (both Step 1 and the existing
123+
`api --ud` handler) should mention `yarn cedar build --ud` explicitly, not just
124+
`yarn cedar build api`, so users understand a plain build is not sufficient.
125+
126+
---
127+
128+
## Affected Files Summary
129+
130+
File: `packages/cli/src/commands/serve.ts`
131+
132+
Change: Add `--ud` to `$0` builder; add UD both-sides handler in `$0` handler;
133+
update middleware check; update error messages
134+
135+
That is the only file that needs to change.
136+
137+
---
138+
139+
## Sequencing
140+
141+
All three steps are in the same file and can land in one PR.
142+
143+
---
144+
145+
## What This Approach Does NOT Include
146+
147+
- **`virtual:cedar-web` / web fallback inside the UD build artifact** — not
148+
needed here. That would only be relevant for single-runtime deployments (e.g.
149+
a single Cloudflare Worker serving both web and API). For the baremetal/nginx
150+
production topology that `cedar serve --ud` is modelling, web assets are
151+
always served by a separate process.
152+
- **Changes to `cedarUniversalDeployPlugin` or `buildUDApiServer`** — the
153+
build pipeline is unchanged.
154+
- **Single-port serving** — two ports (web on `8910`, API on `8911`) is
155+
correct here because it mirrors the two-process nginx topology.
156+
157+
---
158+
159+
## Questions/Answers
160+
161+
1. **`server.ts` / custom Fastify compatibility**: When `--ud` is passed,
162+
should Cedar warn if the project has a custom `api/src/server.ts`? That
163+
file is a Fastify concept and is silently ignored by the UD entry. A
164+
warning here would be better than a silent skip.
165+
166+
Answer: Yes, Cedar should warn if the project has a custom
167+
`api/src/server.ts`. It should acknowledge that the user is testing the
168+
experimental UD support and that it won't match their production Fastify
169+
setup.
170+
171+
2. **Port flags**: The `$0` sub-command currently accepts `--port` (single
172+
port for... something). For the `--ud` path, there are two ports. Should
173+
`--port` set the web port, the API port, or be disallowed in favour of
174+
`--web-port` / `--api-port`? The existing `bothCLIConfig` already has
175+
`--webPort` and `--apiPort` options, so those should be used.
176+
177+
Answer: For `yarn cedar serve --ud`, the `--port` flag should be disallowed.
178+
179+
---
180+
181+
## Exit Criteria
182+
183+
- `yarn cedar build --ud && yarn cedar serve --ud` starts two processes:
184+
the web server on `8910` and the backend on `8911`.
185+
- The web server proxies API requests to the UD API entry.
186+
- `GET /` (and all SPA routes) returns `web/dist/index.html`.
187+
- `GET /.api/functions/graphql` is proxied to the UD API entry and handled by
188+
Yoga.
189+
- `GET /.api/functions/myFunction` is proxied to the UD API entry and handled by
190+
the function handler.
191+
- Without `--ud`, all existing `cedar serve` behavior is unchanged.
192+
- `cedar serve --ud` with a missing `api/dist/ud/index.js` prints a clear error
193+
pointing to `yarn cedar build --ud`.

packages/api-server/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,16 @@
7373
"types": "./dist/cjs/bothCLIConfigHandler.d.ts",
7474
"default": "./dist/cjs/bothCLIConfigHandler.js"
7575
},
76+
"./fastify": {
77+
"import": {
78+
"types": "./dist/fastify.d.ts",
79+
"default": "./dist/fastify.js"
80+
},
81+
"require": {
82+
"types": "./dist/cjs/fastify.d.ts",
83+
"default": "./dist/cjs/fastify.js"
84+
}
85+
},
7686
"./udFetchable": {
7787
"import": {
7888
"types": "./dist/udFetchable.d.ts",

packages/cli/src/commands/__tests__/serve.test.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,100 @@ describe('yarn cedar serve', () => {
162162
}),
163163
)
164164
})
165+
166+
it('Should reject --port flag when --ud is used on both sides', async () => {
167+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
168+
throw new Error('process.exit called')
169+
})
170+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
171+
172+
const parser = yargs().command('serve [side]', false, builder)
173+
174+
await expect(parser.parse('serve --port 9898 --ud')).rejects.toThrow(
175+
'process.exit called',
176+
)
177+
178+
expect(errorSpy).toHaveBeenCalledWith(
179+
expect.stringContaining('--port flag is not supported with --ud'),
180+
)
181+
182+
exitSpy.mockRestore()
183+
errorSpy.mockRestore()
184+
})
185+
186+
it('Should error when UD entry is missing for both sides with --ud', async () => {
187+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
188+
throw new Error('process.exit called')
189+
})
190+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
191+
192+
vi.spyOn(fs, 'existsSync').mockImplementation((pathToCheck) => {
193+
const normalizedPath = pathToCheck.toString().replaceAll('\\', '/')
194+
// UD entry doesn't exist
195+
if (normalizedPath.includes('/mocked/project/api/dist/ud/index.js')) {
196+
return false
197+
}
198+
// web dist exists
199+
if (normalizedPath.includes('/mocked/project/web/dist')) {
200+
return true
201+
}
202+
// api base exists
203+
if (normalizedPath.includes('/mocked/project/api')) {
204+
return true
205+
}
206+
// Don't detect the server file
207+
return !normalizedPath.includes('/mocked/project/api/src/server.')
208+
})
209+
210+
const parser = yargs().command('serve [side]', false, builder)
211+
212+
await expect(parser.parse('serve --ud')).rejects.toThrow(
213+
'process.exit called',
214+
)
215+
216+
expect(errorSpy).toHaveBeenCalledWith(
217+
expect.stringContaining('yarn cedar build --ud'),
218+
)
219+
220+
exitSpy.mockRestore()
221+
errorSpy.mockRestore()
222+
})
223+
224+
it('Should error when web dist is missing for both sides with --ud', async () => {
225+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
226+
throw new Error('process.exit called')
227+
})
228+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
229+
230+
vi.spyOn(fs, 'existsSync').mockImplementation((pathToCheck) => {
231+
const normalizedPath = pathToCheck.toString().replaceAll('\\', '/')
232+
// UD entry exists
233+
if (normalizedPath.includes('/mocked/project/api/dist/ud/index.js')) {
234+
return true
235+
}
236+
// web dist index.html doesn't exist
237+
if (normalizedPath.includes('/mocked/project/web/dist/index.html')) {
238+
return false
239+
}
240+
// api base exists
241+
if (normalizedPath.includes('/mocked/project/api')) {
242+
return true
243+
}
244+
// Don't detect the server file
245+
return !normalizedPath.includes('/mocked/project/api/src/server.')
246+
})
247+
248+
const parser = yargs().command('serve [side]', false, builder)
249+
250+
await expect(parser.parse('serve --ud')).rejects.toThrow(
251+
'process.exit called',
252+
)
253+
254+
expect(errorSpy).toHaveBeenCalledWith(
255+
expect.stringContaining('Web build artifacts not found'),
256+
)
257+
258+
exitSpy.mockRestore()
259+
errorSpy.mockRestore()
260+
})
165261
})

0 commit comments

Comments
 (0)