diff --git a/docs/implementation-docs/2026-03-26-cedarjs-project-overview.md b/docs/implementation-docs/2026-03-26-cedarjs-project-overview.md index c9b7f38675..959b4fd34c 100644 --- a/docs/implementation-docs/2026-03-26-cedarjs-project-overview.md +++ b/docs/implementation-docs/2026-03-26-cedarjs-project-overview.md @@ -195,45 +195,45 @@ Routes.tsx ← 4 routes added inside `. Typed params, globs, redirects, `` layouts, `` auth guards. Named route helpers. Link/navigate/useLocation/useParams. | -| auth | Provider-agnostic. `createAuth(provider)` → {AuthProvider, useAuth}. State: loading/authenticated/user. \*SSR/RSC: ServerAuthProvider injects state for SSR. | -| web | App shell. RedwoodProvider. createCell (GraphQL state→UI). Apollo (useQuery/useMutation). Head/MetaTags. FatalErrorBoundary. Toast. FetchConfig. | -| api | Server runtime. Auth extraction. Validations (validate/validateWith). CORS. Logging (Pino). Cache (Redis/Memcached/InMemory). Webhooks. RedwoodError. | -| graphql-server | Yoga factory. Merge SDLs (schema) + services (resolvers) + directives + subscriptions. Armor. GraphiQL. useRequireAuth. Directive system (validator+transformer). | -| vite | cedar() → Vite plugins. Cell transform, entry injection, auto-imports. \*SSR/RSC: adds Express + 2 Vite servers, RSC transforms, Hot Module Replacement. | -| cli | Yargs. 25+ commands. Generators for all types. Plugin system. Telemetry. .env loading. | -| forms | react-hook-form wrapper. Typed fields. GraphQL coercion (valueAsBoolean/JSON). Error display. | -| prerender | Static Site Generation. renderToString at build, extract react-helmet meta tags, populate Apollo cache, write static HTML. | -| realtime | Live queries + subscriptions. @live directive. createPubSub. InMemory/Redis stores. | -| jobs | Background processing. JobManager/jobs/queues/workers. Delay/waitUntil/cron. Prisma adapter. | -| mailer | Email. Core + handlers (nodemailer/resend/in-memory) + renderers (react-email/mjml). | -| storage | File uploads. setupStorage→Prisma extension. FileSystem/Memory adapters. UrlSigner. | -| record | ActiveRecord on Prisma. Validations, reflections, relations. | -| context | Request-scoped context via AsyncLocalStorage. Proxy-based. Declaration merging. | -| server-store | Per-request store: auth state, headers, cookies, URL. \*SSR/RSC: used by middleware. | -| gqlorm | Prisma API → Proxy → GraphQL. useLiveQuery. Parser+generator. | -| structure | Project model (pages/routes/cells/services/SDLs). Diagnostics. ts-morph. | -| codemods | jscodeshift transforms. Version-organized (v2-v7). Cedar+migration from Redwood. | -| testing | Jest/Vitest config. MockProviders, MockRouter, mockGql, scenario helpers. | -| storybook | Vite Storybook. | -| project-config | Read cedar.toml. getPaths/getConfig/findUp. | -| internal | Re-exports project-config+babel-config. buildApi/dev/generate. Route extraction. | -| api-server | Fastify. Auto-discover Lambda functions. Mount GraphQL. Custom server.ts. | -| web-server | Fastify for web side. Uses fastify-web adapter. | -| fastify-web | Fastify plugin. Static files, SPA fallback, API proxy, prerender. | -| babel-config | Presets/plugins for api+web. registerApiSideBabelHook. | -| eslint-config | Flat config. TS+React+a11y+react-compiler+prettier. | -| eslint-plugin | Rules: process-env-computed, service-type-annotations, unsupported-route-components. | -| create-cedar-app | Standalone scaffolding CLI. Interactive. TS/JS. Copies templates. | -| create-cedar-rsc-app | Standalone RSC scaffolding. Downloads template zip. | -| telemetry | Anonymous CLI telemetry. Duration/errors. | -| tui | Terminal UI. spinners, boxes, reactive updates. | -| ogimage-gen | Vite plugin+middleware. OG images from React components. | -| cookie-jar | Typed cookie map. get/set/has/unset/serialize. | -| utils | Pluralization wrapper. | +| Package | Behavior | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| core | Umbrella. Re-exports CLI, servers, testing, config. Bin shims. | +| router | JSX routing. ``. Typed params, globs, redirects, `` layouts, `` auth guards. Named route helpers. Link/navigate/useLocation/useParams. | +| auth | Provider-agnostic. `createAuth(provider)` → {AuthProvider, useAuth}. State: loading/authenticated/user. \*SSR/RSC: ServerAuthProvider injects state for SSR. | +| web | App shell. RedwoodProvider. createCell (GraphQL state→UI). Apollo (useQuery/useMutation). Head/MetaTags. FatalErrorBoundary. Toast. FetchConfig. | +| api | Server runtime. Auth extraction. Validations (validate/validateWith). CORS. Logging (Pino). Cache (Redis/Memcached/InMemory). Webhooks. RedwoodError. | +| graphql-server | Yoga factory. Merge SDLs (schema) + services (resolvers) + directives + subscriptions. Armor. GraphiQL. useRequireAuth. Directive system (validator+transformer). | +| vite | cedar() → Vite plugins. Cell transform, entry injection, auto-imports. \*SSR/RSC: adds Express + 2 Vite servers, RSC transforms, Hot Module Replacement. | +| cli | Yargs. 25+ commands. Generators for all types. Plugin system. Telemetry. .env loading. | +| forms | react-hook-form wrapper. Typed fields. GraphQL coercion (valueAsBoolean/JSON). Error display. | +| prerender | Static Site Generation. renderToString at build, extract react-helmet meta tags, populate Apollo cache, write static HTML. | +| realtime | Live queries + subscriptions. @live directive. createPubSub. InMemory/Redis stores. | +| jobs | Background processing. JobManager/jobs/queues/workers. Delay/waitUntil/cron. Prisma adapter. | +| mailer | Email. Core + handlers (nodemailer/resend/in-memory) + renderers (react-email/mjml). | +| storage | File uploads. setupStorage→Prisma extension. FileSystem/Memory adapters. UrlSigner. | +| record | ActiveRecord on Prisma. Validations, reflections, relations. | +| context | Request-scoped context via AsyncLocalStorage. Proxy-based. Declaration merging. | +| server-store | Per-request store: auth state, headers, cookies, URL. \*SSR/RSC: used by middleware. | +| gqlorm | Prisma API → Proxy → GraphQL. useLiveQuery. Parser+generator. | +| structure | Project model (pages/routes/cells/services/SDLs). Diagnostics. ts-morph. | +| codemods | jscodeshift transforms. Version-organized (v2-v7). Cedar+migration from Redwood. | +| testing | Jest/Vitest config. MockProviders, MockRouter, mockGql, scenario helpers. | +| storybook | Vite Storybook. | +| project-config | Read cedar.toml. getPaths/getConfig/findUp. | +| internal | Re-exports project-config+babel-config. buildApi/dev/generate. Route extraction. | +| api-server | Fastify (default) + srvx/WinterTC (opt-in via `cedar serve api --ud` or `cedar-ud-server` binary). Auto-discover Lambda functions. Mount GraphQL. Custom server.ts. srvx path uses `buildCedarDispatcher` + `createUDServer` for Fastify-free serving. | +| web-server | Fastify for web side. Uses fastify-web adapter. | +| fastify-web | Fastify plugin. Static files, SPA fallback, API proxy, prerender. | +| babel-config | Presets/plugins for api+web. registerApiSideBabelHook. | +| eslint-config | Flat config. TS+React+a11y+react-compiler+prettier. | +| eslint-plugin | Rules: process-env-computed, service-type-annotations, unsupported-route-components. | +| create-cedar-app | Standalone scaffolding CLI. Interactive. TS/JS. Copies templates. | +| create-cedar-rsc-app | Standalone RSC scaffolding. Downloads template zip. | +| telemetry | Anonymous CLI telemetry. Duration/errors. | +| tui | Terminal UI. spinners, boxes, reactive updates. | +| ogimage-gen | Vite plugin+middleware. OG images from React components. | +| cookie-jar | Typed cookie map. get/set/has/unset/serialize. | +| utils | Pluralization wrapper. | ## CONVENTIONS @@ -250,6 +250,6 @@ Routes.tsx ← 4 routes added inside { - const ctx = await buildCedarContext(request) - return handleRequest(request, ctx) - }, - } - ``` -- Integrate `@universal-deploy/store`: call `addEntry()` for each - Cedar server entry (GraphQL, auth, filesystem functions) during the - build -- Validate self-hosting using `@universal-deploy/adapter-node`, which - wraps store entries with `srvx` + `sirv` — Cedar does not implement - any Node HTTP handling itself -- Validate Netlify deployment using `@universal-deploy/adapter-netlify` - as an early end-to-end check -- Confirm `yarn rw serve` delegates to UD's node adapter rather than - Fastify +- Implement `buildCedarDispatcher(options)` in `@cedarjs/api-server`: + discovers API functions from `api/dist/functions/` at runtime, + builds a rou3 router and per-function `Fetchable` map, and returns a + single dispatch `Fetchable` together with the `EntryMeta[]` needed to + register each function with the UD store +- Implement `createUDServer(options)` in `@cedarjs/api-server`: wraps + `buildCedarDispatcher` in an srvx HTTP server and calls `addEntry()` + for each discovered function for UD store introspection +- Expose `cedar-ud-server` binary and `cedar serve api --ud` CLI flag, + both delegating to `createUDServer` instead of Fastify + +#### Why `@universal-deploy/node` proper is a Phase 4 concern + +`@universal-deploy/node` is designed to be consumed through a Vite +build pipeline. Its server entry (`@universal-deploy/node/serve`) +starts srvx by statically importing the catch-all handler as a virtual +module: + +```ts +// @universal-deploy/node/serve (simplified) +import userServerEntry from 'virtual:ud:catch-all' +// srvx then calls userServerEntry.fetch for every request +``` + +`virtual:ud:catch-all` is not a real module path — it only resolves +during a Vite build. Cedar's API side is currently compiled with +Babel/esbuild, not Vite, so `@universal-deploy/node/serve` cannot be +imported or run for `cedar serve api` today. + +Phase 3's `createUDServer` is the practical equivalent for the +current build pipeline: it uses the same srvx server and produces +identical runtime behaviour, discovering and loading functions from +the already-compiled `api/dist/functions/` at startup rather than +through a Vite virtual module graph. + +#### How to wire in `@universal-deploy/node` once Phase 4 is done + +When Phase 4 gives Cedar a Vite-based API server build, the hookup is +straightforward: + +1. Introduce `cedarUniversalDeployPlugin()` in `@cedarjs/vite` and add + it to the **API server Vite build config** (not the web client + config — the plugin resolves API-server virtual modules that have + no relevance to the browser bundle) +2. Wire `virtual:ud:catch-all` → `virtual:cedar-api` inside the plugin + so that `@universal-deploy/node/serve` can import Cedar's aggregate + Fetchable at build time +3. Add `node()` from `@universal-deploy/node/vite` to the same + **API server Vite build config** +4. `cedar serve` runs the Vite-built output directly + +**Naming caution for Phase 4**: Vite calls its Node.js server build +environment **"SSR"** regardless of whether it renders HTML. This is +confusing in Cedar's context, where "SSR" specifically means React +streaming / RSC. The Vite "SSR environment" output that +`@universal-deploy/node` produces is purely the API server entry — it +has no connection to Cedar's HTML SSR feature. Do not add `node()` to +any Vite config that also builds the HTML SSR entry. #### Deliverables -- `buildCedarContext` utility in a shared framework package -- Build tooling that emits `Fetchable` entries per Cedar server entry -- `@universal-deploy/store` integration (`addEntry` calls at build time) -- Validated self-hosting via `@universal-deploy/adapter-node` +- `buildCedarDispatcher(options)` — runtime function discovery and + Fetchable dispatch, in `@cedarjs/api-server` +- `createUDServer(options)` — srvx-based API server wrapping the + dispatcher, in `@cedarjs/api-server` +- `cedar-ud-server` binary and `cedar serve api --ud` flag — serve + the Cedar API without Fastify #### Exit Criteria -- Cedar can run in production on Node without Fastify, using - `@universal-deploy/adapter-node` -- Cedar's server entries are registered in the UD store at build time -- `yarn rw serve` no longer depends on the Fastify-first API server - architecture +- Cedar can run in production on Node without Fastify via + `cedar serve api --ud` or the `cedar-ud-server` binary + +#### Temporary scaffolding introduced in Phase 3 + +Several pieces of Phase 3 are deliberate scaffolding — they make Cedar +work without Fastify today while the Vite-based build pipeline that +`@universal-deploy/node` requires does not yet exist. They should be +removed or replaced in the phases noted below. + +**Remove / replace in Phase 4:** + +- `createUDServer` (`packages/api-server/src/createUDServer.ts`) — + the srvx runtime stand-in for `@universal-deploy/node`. Phase 4 + replaces it with a Vite-built server entry produced by + `@universal-deploy/node/vite`'s `node()` plugin. Once `cedar serve` + runs that built output, `createUDServer` has no remaining purpose + and should be deleted. +- `udBin.ts` / `udCLIConfig.ts` / the `cedar-ud-server` binary — + these exist solely to invoke `createUDServer`. They go away together + with it in Phase 4, unless a non-Vite standalone serve mode is + deliberately kept. +- `cedar serve api --ud` CLI flag (`packages/cli/src/commands/serve.ts`) + — the temporary bridge that routes to `createUDServer` instead of + Fastify. Phase 4 should make UD serving the default and remove the + flag entirely. +- `buildCedarDispatcher` (`packages/api-server/src/udDispatcher.ts`) — + the runtime function-discovery function (uses `fast-glob` to scan + `api/dist/functions/` at startup). In Phase 4 the API is built and + bundled by Vite, so runtime discovery is no longer needed; the + function can be deleted. If a non-Vite standalone mode is kept, + `buildCedarDispatcher` can be retained for that path only. **User-facing impact**: None for most developers. Self-hosting users -get a simpler, Fastify-free production server backed by UD's node -adapter. +can opt in to the Fastify-free srvx server via `cedar serve api --ud` +or the `cedar-ud-server` binary. Full `@universal-deploy/node` +end-to-end arrives in Phase 4. --- @@ -748,34 +818,80 @@ Depends on Phases 2 and 3. #### Goal Replace the current web+API split dev model with a single Vite-hosted -development entrypoint. +development entrypoint for Cedar's default runtime path, while preserving +a compatibility path for existing apps that depend on custom Fastify +server setup. #### Work -- Eliminate the `8910 → proxy → 8911` mental model +- Eliminate the `8910 → proxy → 8911` mental model for the default + Cedar runtime path - Route page, GraphQL, auth, and function requests through one - externally visible dev host + externally visible dev host on that default path - Integrate backend handler execution into the Vite dev runtime (likely via Vite's `server.middlewareMode` or custom plugin) - Ensure server-side file watching and invalidation work for backend entries - Preserve strong DX for browser requests, direct `curl` requests, and GraphQL tooling (e.g., GraphiQL must still work) +- Preserve a compatibility path for apps that use `api/src/server.{ts,js}`, + `configureFastify`, `configureApiServer`, or direct Fastify plugin + registration, rather than silently routing them through the new + default runtime and dropping supported behavior +- Introduce `cedarUniversalDeployPlugin()` in `@cedarjs/vite` and wire + it into the **API server Vite build config**: register + `virtual:cedar-api` with the UD store via `addEntry()`, resolve + `virtual:ud:catch-all` → `virtual:cedar-api`, and export the Cedar + API Fetchable as the virtual module's default export. This plugin + belongs to the API server build — not the web client build — because + it resolves API-server virtual modules that have no relevance to the + browser bundle. When the plugin is introduced, add + `@cedarjs/api-server` as a `peerDependency` of `@cedarjs/vite` in + `packages/vite/package.json` — the virtual module emitted by the + plugin imports `buildCedarDispatcher` from `@cedarjs/api-server`, so + consumers need it installed alongside `@cedarjs/vite` +- Add `node()` from `@universal-deploy/node/vite` to the same API + server Vite build config (not the web client config, and not the + HTML SSR config — see naming caution below). After this, + `cedar serve` runs the Vite-built server entry instead of `createUDServer` + +**Naming caution**: Vite calls its Node.js server build environment +**"SSR"** regardless of whether it renders HTML. This is confusing in +Cedar's context, where "SSR" specifically means React streaming / RSC. +The Vite "SSR environment" output that `@universal-deploy/node` +produces is purely the API server entry — it has no connection to +Cedar's HTML SSR feature. Do not add `node()` to any Vite config that +also builds the HTML SSR entry. #### Deliverables -- One visible development port -- One dev request dispatcher -- One shared module graph for frontend and backend development +- One visible development port on the default runtime path +- One dev request dispatcher on the default runtime path +- One shared module graph for frontend and backend development on the + default runtime path +- A documented compatibility path for apps with custom Fastify server + setup +- `@universal-deploy/node` wired end-to-end: Vite builds a + self-contained server entry; `cedar serve` runs it on the default + runtime path #### Exit Criteria -- Cedar dev no longer requires a separately exposed backend port +- Cedar dev no longer requires a separately exposed backend port on the + default runtime path - Requests to functions and GraphQL can be made directly against the - Vite dev host - -**User-facing impact**: High (positive). Developers see one port, one -process, simpler mental model. Config files may need minor updates. + Vite dev host on the default runtime path +- `cedar serve` runs an `@universal-deploy/node`-built server entry on + the default runtime path, completing the Phase 3 goal of removing + Fastify from that production path +- Existing apps with custom Fastify server setup still have a supported + compatibility path and are not silently forced onto the new default + runtime + +**User-facing impact**: High (positive). Most developers see one port, +one process, and a simpler mental model. Existing apps with custom +Fastify setup remain on a compatibility path until a later migration +story exists. Config files may need minor updates. --- @@ -787,37 +903,58 @@ Depends on Phase 4. #### Goal -Promote the initial `addEntry()` wiring from Phase 3 into a -first-class Cedar Vite plugin in `@cedarjs/vite`. Phase 3 gets Cedar -running without Fastify using UD's adapters; Phase 5 makes the -integration complete, correct, and provider-discoverable. +Expand `cedarUniversalDeployPlugin()` (introduced in Phase 4 as part +of the API server Vite build) from a single aggregate entry into a +complete, per-route registration that UD adapters and provider plugins +can rely on. Phase 4 ships a working plugin with one catch-all entry; +Phase 5 makes it correct and provider-discoverable. + +#### Current state after Phase 4 + +`cedarUniversalDeployPlugin()` exists (introduced in Phase 4) and +provides: + +- A single aggregate `virtual:cedar-api` entry registered with + `addEntry()`, covering all Cedar API routes via one catch-all + Fetchable +- `virtual:cedar-api` virtual module: exports Cedar's API Fetchable + so UD adapters can consume it +- `virtual:ud:catch-all` → `virtual:cedar-api` resolution: routes + the UD catch-all ID (used by `@universal-deploy/node/serve`) to + Cedar's aggregate API entry #### Work -- Extract the `addEntry()` calls from Phase 3's ad-hoc build wiring - into a formal `@cedarjs/vite` plugin +- Replace the single `virtual:cedar-api` aggregate entry with + per-function entries derived from Cedar's route manifest (Phase 2), + so providers that benefit from per-route isolation (e.g., Cloudflare + Workers) can split on individual functions - Ensure all Cedar server entries are registered with the correct - `route`, `method`, and `environment` metadata that UD and provider - plugins need: - - web catch-all SSR entry (or SPA fallback) + `route`, `method`, and `environment` metadata: - GraphQL entry - auth entry - filesystem-discovered function entries -- Align Cedar's internal `CedarRouteRecord` manifest (from Phase 2) - with the `EntryMeta` shape UD's store expects — Cedar should derive - UD entries from its own route manifest, not maintain them separately -- Validate the plugin against `@universal-deploy/adapter-node` and + - web catch-all / SPA fallback (web side) +- Align Cedar's `CedarRouteRecord` manifest (Phase 2) with the + `EntryMeta` shape UD's store expects — entries should be derived + from the manifest, not maintained separately +- Update `virtual:ud:catch-all` to generate a proper multi-route + dispatcher (using rou3 across all registered entries) rather than + the simple single-entry re-export from Phase 4 +- Validate the plugin against `@universal-deploy/node` and `@universal-deploy/adapter-netlify` - Document the plugin's role so future UD adapter authors know what Cedar registers and in what shape #### Deliverables -- `@cedarjs/vite` Cedar UD plugin +- `cedarUniversalDeployPlugin()` expanded with per-route entries + from Cedar's route manifest - All Cedar server entries registered via `addEntry()` with complete metadata at Vite/plugin time - Cedar's route manifest and UD's store in sync from a single source of truth +- Validated against `@universal-deploy/node` end-to-end #### Exit Criteria @@ -885,9 +1022,9 @@ targets Cedar cares about. #### Work - Validate Netlify and Vercel first (largest user base) -- Validate Node/self-hosted via `@universal-deploy/adapter-node` +- Validate Node/self-hosted via `@universal-deploy/node` - Optionally validate Cloudflare after the first pass -- Use UD's adapters (`@universal-deploy/adapter-node`, +- Use UD's adapters (`@universal-deploy/node`, `@universal-deploy/adapter-netlify`, and equivalent) — Cedar builds none of its own - Test: @@ -938,8 +1075,11 @@ nothing.** developer's perspective. UD's node adapter is wired up but used only for production self-hosting. Dev still uses two ports. -**After Phase 4**: Single-port dev. This is the first major visible -change. Developers update their config and enjoy a simpler mental model. +**After Phase 4**: Single-port dev on the default runtime path. This is +the first major visible change. Developers on the standard Cedar path +update their config and enjoy a simpler mental model. Apps with custom +Fastify server setup remain on a compatibility path rather than being +silently forced onto the new runtime. **After Phase 5**: No visible change for developers. UD integration is framework-internal. @@ -1009,18 +1149,20 @@ impact (Phases 4, 6, 7). The guide should cover: - Step-by-step migration instructions - Before/after code examples - Common pitfalls +- How to identify whether an app is on the default runtime path or the + custom Fastify compatibility path ### Which Phases Require App Developer Action -| Phase | App Developer Action Required | -| ----- | ------------------------------------------- | -| 1 | None (shim handles it) | -| 2 | None | -| 3 | None | -| 4 | Config updates, possible dev script changes | -| 5 | None | -| 6 | SSR config migration | -| 7 | Deploy config updates | +| Phase | App Developer Action Required | +| ----- | ----------------------------------------------------------------------------------- | +| 1 | None (shim handles it) | +| 2 | None | +| 3 | None | +| 4 | Config updates for standard apps; compatibility-path review for custom Fastify apps | +| 5 | None | +| 6 | SSR config migration | +| 7 | Deploy config updates | ## Risks @@ -1038,6 +1180,9 @@ impact (Phases 4, 6, 7). The guide should cover: to edge cases in existing auth middleware - Phase 4 (Vite-centric dev) being significantly harder than estimated due to HMR, module graph, and backend file watching interactions +- Silently dropping supported Fastify-specific behavior for existing + apps that use `api/src/server.{ts,js}`, `configureFastify`, + `configureApiServer`, or direct Fastify plugin registration ## Open Questions diff --git a/docs/implementation-plans/universal-deploy-phase-4-detailed-plan.md b/docs/implementation-plans/universal-deploy-phase-4-detailed-plan.md new file mode 100644 index 0000000000..2344fdafee --- /dev/null +++ b/docs/implementation-plans/universal-deploy-phase-4-detailed-plan.md @@ -0,0 +1,1125 @@ +# Detailed Plan: Universal Deploy Phase 4 — Vite-Centric Full-Stack Dev Runtime + +## Summary + +Phase 4 is the point where Cedar's Universal Deploy work becomes visible in +day-to-day development. The core shift is not just "use Vite more"; it is +"make Vite the single externally visible development runtime for the whole app." + +Today, Cedar development is still mentally and operationally split: + +- the web side is served through Vite +- the API side runs as a separate backend process +- requests move through a proxy boundary +- backend invalidation and frontend invalidation are related, but not truly part + of one runtime model + +Phase 4 replaces that with a single dev host and a single request entrypoint +that can serve: + +- web assets and HTML +- GraphQL requests +- auth requests +- server function requests +- future fetch-native backend handlers + +This phase is also where the temporary Phase 3 scaffolding starts turning into a +real runtime architecture. In particular: + +- Cedar dev should stop exposing a separate backend port as part of the normal + developer experience +- the API runtime should execute inside a Vite-centric development environment +- the API server Vite build should gain the first real + `cedarUniversalDeployPlugin()` +- `@universal-deploy/node` should be wired into the API server build and serve + path so `cedar serve` runs the Vite-built server entry rather than the + temporary direct server construction path + +Phase 4 is still not the phase where Cedar fully formalises per-route UD entry +registration. That belongs to Phase 5. Phase 4 should intentionally ship a +working aggregate-entry model that is operationally correct for local +development and for the Node serve path. + +## Why Phase 4 Exists + +Phases 1-3 establish the prerequisites: + +- Phase 1 makes Cedar handlers fetch-native +- Phase 2 gives Cedar a formal backend route manifest +- Phase 3 adopts UD deployment adapters and introduces temporary scaffolding + +But none of that yet changes the main development experience enough. Cedar still +feels like a split system unless development itself is unified. + +Phase 4 exists to solve five concrete problems: + +1. **Port split** + - Developers should not need to think in terms of "frontend port" and + "backend port" for normal app development. + +2. **Proxy split** + - Requests should not conceptually travel from "the Vite server" to "the API + server" as two separate application runtimes. + +3. **Module graph split** + - Backend code changes should participate in a Vite-owned invalidation and + restart model rather than a separate watcher/process model. + +4. **Serve path split** + - `cedar serve` should move onto the same UD-oriented build output that the + broader integration is targeting. + +5. **Architecture split** + - Cedar should stop treating the API runtime as a special non-Vite island in + development. + +## Relationship to the Refined Integration Plan + +This document expands the refined plan's Phase 4 section into an implementation +plan with concrete architecture, workstreams, sequencing, risks, and acceptance +criteria. + +It preserves the refined plan's key constraints: + +- one visible development port +- one dev request dispatcher +- backend execution integrated into the Vite dev runtime +- strong DX for browser traffic and direct HTTP tooling +- `cedarUniversalDeployPlugin()` introduced in the API server Vite build +- `node()` from `@universal-deploy/node/vite` added to the API server Vite build +- no confusion between Vite's "SSR environment" and Cedar's HTML SSR feature + +It also preserves the phase boundary: + +- Phase 4 delivers a working aggregate-entry plugin and runtime +- Phase 5 expands that into full per-route registration and provider-facing + correctness + +## Goals + +### Primary Goals + +- Make `cedar dev` expose one externally visible host/port for the default app + runtime +- Route web and API traffic through one development dispatcher for the default + Cedar runtime path +- Execute Cedar-owned backend handlers in a Vite-centric runtime +- Ensure backend source changes are reflected through a coherent dev invalidation + model +- Make `cedar serve` run the Vite-built UD Node server entry for the default + non-custom-server path +- Introduce the first production-worthy version of + `cedarUniversalDeployPlugin()` for the API server build +- Preserve a compatibility lane for apps that depend on custom Fastify server + setup + +### Secondary Goals + +- Preserve GraphiQL and direct `curl` workflows +- Preserve existing auth and function behavior during the transition +- Minimise app-level migration burden +- Keep Phase 4 compatible with the later Phase 5 route-registration expansion +- Make the compatibility story for `api/src/server.{ts,js}`, + `configureFastify`, and custom Fastify plugins explicit + +## Non-Goals + +Phase 4 should explicitly not try to do all of the following: + +- rebuild Cedar HTML SSR or RSC +- formalise per-route UD registration for all providers +- redesign Cedar's web-side production serving model +- solve every provider-specific deployment concern +- remove all transitional compatibility layers introduced earlier +- merge web and API build outputs into one universal production artifact +- introduce a new public app authoring API unless required for runtime + correctness +- remove the custom Fastify server path for apps that already depend on it +- force all existing Fastify-specific customisations onto the new runtime in + this phase + +## Current Baseline Before Phase 4 + +Based on the current Cedar architecture, the refined integration plan, and the +current codebase, the baseline is: + +- web development is Vite-centric +- API development is still conceptually separate +- `cedar dev` still starts separate web and API jobs +- the current web/API relationship still assumes a proxy-oriented model in + important places +- production API serving has a temporary UD-oriented path +- Cedar already has or is expected to have: + - fetch-native handlers + - a backend route manifest + - temporary UD scaffolding +- `cedar serve api --ud` or equivalent transitional paths exist, but they are + not yet the default unified runtime story +- Cedar still has a real, supported Fastify customisation surface through + `api/src/server.{ts,js}`, `configureApiServer`, and older + `configureFastify`-style configuration +- the current UD dispatcher is an aggregate Cedar API dispatcher, but it is not + yet a complete replacement for arbitrary Fastify custom routes, hooks, + decorators, or plugins + +This means Phase 4 is not starting from zero. It is integrating already-created +pieces into a coherent dev runtime while preserving a compatibility path for +apps that depend on Fastify-specific server customisation. + +## Codebase Alignment Notes + +The current codebase already supports the main direction of this phase: + +- temporary UD scaffolding exists specifically to be removed in Phase 4 +- a shared aggregate Cedar dispatcher already exists and is intended to be used + by both the temporary server path and the future Vite virtual module path +- the CLI already marks the current UD serve path as transitional +- the current dev model is still clearly split between web and API processes + +At the same time, the codebase also makes two important constraints visible: + +1. The current aggregate UD dispatcher is still narrower than the final Phase 4 + target. It already handles Cedar-owned API surfaces such as GraphQL and + filesystem-discovered functions, but it should not be treated as proof that + all Fastify-based customisation has already been subsumed by the fetch-native + runtime. +2. Cedar currently exposes a real Fastify customisation surface. That means + Phase 4 cannot be treated as a blanket removal of Fastify from every app + runtime path without breaking supported user setups. + +These constraints shape the recommended implementation approach for this phase: +the unified Vite-centric runtime becomes the default path for standard Cedar +apps, while custom-server apps remain on an explicit compatibility lane until a +later migration path exists. + +## Architectural Target for Phase 4 + +### High-Level Shape + +After Phase 4, the default development architecture should look like this: + +- one Vite-hosted dev server is externally visible +- that dev server owns the request entrypoint +- browser-facing web requests are handled by Vite as usual +- API-like requests are dispatched into Cedar's fetch-native backend runtime +- backend modules are loaded through a Vite-aware mechanism rather than a + completely separate long-lived backend process +- the API server build has a Vite config that: + - installs `cedarUniversalDeployPlugin()` + - installs `node()` from `@universal-deploy/node/vite` + - emits a self-contained Node server entry for `cedar serve` + +For apps with custom Fastify setup, Phase 4 should preserve a compatibility +lane rather than forcing them onto the default unified runtime immediately. +Those apps may continue to use a custom-server path until Cedar provides a +framework-agnostic replacement for the Fastify-specific extension points they +depend on. + +### Conceptual Request Flow in Dev + +The intended request flow is: + +1. request arrives at the single Vite dev host +2. Cedar dev middleware classifies the request +3. request is dispatched to one of: + - Vite static/HMR/web handling + - GraphQL handler + - auth handler + - function handler + - aggregate Cedar API dispatcher +4. response is returned directly from the same host + +The important change is that the browser, GraphQL clients, auth callbacks, and +CLI HTTP tooling all target the same visible origin. + +### Conceptual Build/Serve Flow + +For `cedar serve`, the intended flow is: + +1. API server Vite config builds the server entry +2. `cedarUniversalDeployPlugin()` registers Cedar's aggregate API entry +3. `node()` from `@universal-deploy/node/vite` produces the Node-compatible + server output +4. `cedar serve` launches that built server entry + +This completes the move away from the temporary direct server construction path +for the Node serve case. + +## Design Principles for This Phase + +### 1. Vite Owns the Dev Host + +The visible development host should be Vite's host, not a wrapper process that +merely proxies to Vite. Cedar may compose middleware around Vite, but the +developer mental model should still be "the app runs on one Vite dev server." + +### 2. Cedar Owns Request Classification + +Vite should remain the host, but Cedar should own the logic that decides whether +a request is: + +- a frontend asset/HMR request +- a page/document request +- a GraphQL request +- an auth request +- a server function request +- a fallback request + +This keeps Cedar's routing and runtime contract authoritative. + +### 3. Fetch-Native Execution Is the Runtime Center + +Backend execution should happen through Cedar's fetch-native handler contract, +not through reintroduced Node/Express/Fastify-specific request objects. + +### 4. Aggregate Entry First, Per-Route Later + +Phase 4 should use one aggregate Cedar API entry for correctness and speed of +delivery. It should not prematurely implement the full Phase 5 route-splitting +model. + +### 5. No Cedar/SSR Terminology Drift + +Any Vite config or code comments must clearly distinguish: + +- Vite "SSR" meaning server-side module execution/build target +- Cedar "SSR" meaning HTML server rendering / streaming / RSC-related behavior + +This distinction matters because the API server build will use Vite's server +build machinery without implying Cedar HTML SSR. + +### 6. Preserve Existing App Contracts Where Possible + +App authors should not need to rewrite routes, functions, GraphQL handlers, or +auth setup just to adopt Phase 4. + +### 7. Preserve a Compatibility Lane for Custom Fastify Apps + +Apps that use `api/src/server.{ts,js}`, `configureApiServer`, +`configureFastify`, or direct `server.register(...)` Fastify plugin setup are +using a supported Cedar extension path today. Phase 4 should not silently +bypass or ignore those customisations. + +Instead, the default unified runtime should apply to standard Cedar apps, while +custom-server apps remain on an explicit compatibility lane until Cedar offers a +clear migration path to framework-agnostic extension points. + +## Proposed Runtime Architecture + +## 1. Dev Runtime Composition + +The Phase 4 dev runtime should be composed from three layers: + +### Layer A: Vite Dev Server + +Responsibilities: + +- static asset serving +- HMR +- HTML transforms +- frontend module graph ownership +- browser-facing dev ergonomics + +### Layer B: Cedar Dev Request Dispatcher + +Responsibilities: + +- classify incoming requests +- decide whether Cedar backend handling should run +- invoke the aggregate Cedar API fetch dispatcher when appropriate +- fall through to Vite web handling when appropriate + +This is the key new Phase 4 layer. + +### Layer C: Cedar Aggregate API Runtime + +Responsibilities: + +- execute GraphQL +- execute auth endpoints +- execute filesystem-discovered functions +- execute any other Cedar-owned fetch-native backend entries included in the + aggregate dispatcher + +This layer should be built on the Phase 1 and Phase 2 contracts, not on legacy +event-shaped APIs. + +### Important Scope Note + +In the current codebase, the aggregate UD dispatcher should be treated as the +Cedar-owned backend runtime path, not as a complete replacement for arbitrary +Fastify customisation. Phase 4 should unify Cedar's default runtime path first, +while preserving a separate compatibility lane for apps that depend on +Fastify-specific hooks, decorators, routes, or plugins. + +## 2. Request Classification Model + +The dispatcher should classify requests in a deterministic order. A practical +order is: + +1. Vite internal requests + - HMR endpoints + - Vite client assets + - transformed module requests +2. explicit API endpoints + - GraphQL + - auth + - function routes +3. web asset requests + - static files + - known web assets +4. page/document requests + - app routes that should return the web app shell in SPA mode +5. fallback/error handling + +The exact path patterns should come from Cedar configuration and route manifest +data where possible, not from scattered hardcoded checks. + +### Why Ordering Matters + +Ordering mistakes can create subtle bugs: + +- Vite HMR requests accidentally routed into Cedar API handling +- GraphQL requests falling through to SPA HTML +- auth callback routes being treated as frontend routes +- static assets being intercepted by API logic + +Phase 4 should therefore define request classification as a first-class runtime +concern, not an incidental middleware detail. + +## 3. Backend Execution Model in Dev + +There are two broad implementation styles Cedar could take: + +### Option A: In-Process Vite Middleware Execution + +Cedar installs middleware into the Vite dev server and directly invokes the +aggregate fetch dispatcher from there. + +**Pros** + +- simplest mental model +- one visible server +- minimal extra process orchestration +- easiest path to "one dispatcher" + +**Cons** + +- backend invalidation semantics must be handled carefully +- Node-only backend dependencies must coexist with Vite's server runtime model +- error isolation may be weaker than a separate worker model + +### Option B: Vite-Owned Host with Internal Backend Worker + +Cedar still exposes one visible Vite host, but backend execution happens in an +internal worker/sub-runtime managed by the dev system. + +**Pros** + +- stronger isolation +- potentially cleaner backend reload semantics + +**Cons** + +- more moving parts +- easier to accidentally recreate the old split model internally +- more complexity for Phase 4 than likely necessary + +### Recommendation + +Phase 4 should prefer **Option A** unless implementation evidence proves it +unworkable. The refined plan already points toward Vite middleware integration, +and that is the shortest path to the intended developer experience. + +If isolation issues appear, they should be documented as follow-up work rather +than causing Phase 4 to balloon into a multi-runtime orchestration project. + +## 4. Backend Invalidation and Reload Strategy + +This is one of the most important implementation details. + +The backend runtime must respond correctly to changes in: + +- `api/src/functions/**` +- `api/src/graphql/**` +- `api/src/services/**` +- auth-related backend modules +- route manifest inputs +- generated artifacts that affect backend execution + +### Required Outcomes + +- code changes should be reflected without requiring manual process restarts +- stale backend modules should not remain cached indefinitely +- errors should surface clearly in the terminal and browser/client responses +- invalidation should be targeted enough to avoid unnecessary full reloads when + possible + +### Practical Strategy + +Phase 4 should start with a conservative invalidation model: + +- treat the aggregate Cedar API runtime as a reloadable server module boundary +- when backend-relevant files change, invalidate the aggregate backend entry and + its dependent modules +- rebuild or re-import the backend dispatcher through Vite's server module + system +- prefer correctness over maximal granularity + +This is another place where Phase 5 can later improve precision once per-route +entries exist. + +### Important Constraint + +Do not try to make backend invalidation mirror frontend HMR exactly. Backend +execution correctness matters more than preserving stateful hot replacement +semantics. + +## 5. Aggregate API Entry Shape + +Phase 4 should introduce a single aggregate virtual entry, likely represented by +`virtual:cedar-api`. + +That virtual module should: + +- import the Cedar API dispatcher construction logic +- build the aggregate fetchable from Cedar's route/function/GraphQL/auth sources +- export the aggregate fetchable as the default export + +This entry is the bridge between Cedar's runtime model and UD's Vite/plugin +model. + +### Why Aggregate Entry Is Correct for Phase 4 + +An aggregate entry: + +- keeps plugin complexity manageable +- avoids premature provider-specific route splitting +- is sufficient for local dev and Node serve +- aligns with the refined plan's explicit Phase 4/Phase 5 boundary + +## 6. `cedarUniversalDeployPlugin()` Responsibilities in Phase 4 + +The plugin introduced in this phase should do exactly the minimum needed for a +working system on the default Cedar runtime path. + +### Required Responsibilities + +- register `virtual:cedar-api` with the UD store via `addEntry()` +- resolve `virtual:ud:catch-all` to `virtual:cedar-api` +- emit the virtual module that exports Cedar's aggregate API fetchable +- operate in the API server Vite build, not the web client build + +### Explicit Non-Responsibilities in Phase 4 + +- registering every Cedar route as a separate UD entry +- becoming the final provider-facing route metadata source +- handling web-side route registration comprehensively +- solving all adapter-specific optimisations + +### Package Boundary Implication + +Because the virtual module imports API-server runtime code, +`@cedarjs/vite` should declare `@cedarjs/api-server` as a `peerDependency`, +matching the refined plan. + +## 7. `@universal-deploy/node` Integration in Phase 4 + +The API server Vite build should add `node()` from +`@universal-deploy/node/vite`. + +### Purpose + +- produce a self-contained Node server entry +- let `cedar serve` run the built output +- replace the temporary direct server construction path for the Node serve case + +### Important Clarification + +This is a Vite server build concern, not a Cedar HTML SSR concern. + +Any implementation notes, config names, comments, and docs should repeatedly +make this clear to avoid future confusion. + +## Runtime Lanes in Phase 4 + +Phase 4 should explicitly support two runtime lanes. + +### Lane A: Default Unified Runtime + +This is the primary Phase 4 target for standard Cedar apps: + +- one visible Vite-hosted dev port +- one Cedar dev request dispatcher +- Cedar-owned backend execution through the aggregate fetch-native runtime +- API server Vite build integrated with `cedarUniversalDeployPlugin()` +- `cedar serve` running the Vite-built UD Node server entry + +### Lane B: Custom Fastify Compatibility Runtime + +This lane exists for apps that depend on Cedar's current Fastify-specific server +extension points, including: + +- `api/src/server.{ts,js}` +- `configureApiServer` +- `configureFastify` +- direct `server.register(...)` plugin setup +- custom Fastify routes, hooks, decorators, parsers, or reply/request behavior + +For these apps, Phase 4 should preserve a supported compatibility path rather +than forcing immediate migration. + +### Runtime Selection Rule + +The implementation should treat the presence of a custom server path as a +meaningful runtime distinction. If an app is using a custom server entry or +Fastify-specific setup, Cedar should either: + +- keep that app on the compatibility lane automatically, or +- fail clearly with guidance rather than silently dropping custom behaviour + +Silent partial compatibility is the worst outcome here. + +## Workstreams + +## Workstream 1: Inventory and Stabilise Existing Dev Entry Logic + +### Objective + +Understand and isolate the current `cedar dev` orchestration points so Phase 4 +can replace the split runtime without regressing unrelated behavior. + +### Tasks + +- identify the current web dev startup path +- identify the current API dev startup path +- identify where proxying between web and API currently happens +- identify how GraphQL, auth, and functions are currently mounted in dev +- identify current file watching and restart behavior for backend code +- identify any assumptions in CLI output, port reporting, or generated URLs that + depend on separate web/API ports +- identify all current custom-server and Fastify-specific extension points that + must remain supported on the compatibility lane +- identify where `serverFileExists()` and related custom-server branching + already exist so Phase 4 can build on those distinctions rather than fighting + them + +### Deliverable + +A concrete map of the current dev orchestration points and the minimum set of +places that must change. + +### Notes + +This work should be done before major implementation begins. Phase 4 will be +much riskier if the current split behavior is only partially understood. + +## Workstream 2: Define the Dev Request Dispatcher Contract + +### Objective + +Create a clear internal contract for the single dev dispatcher. + +### Proposed Internal Contract + +The dispatcher should accept: + +- the incoming request +- enough runtime context to classify the request +- access to the aggregate Cedar API fetch handler +- access to Vite's fallback handling path + +And it should return either: + +- a completed response +- a signal to continue into Vite web handling + +### Tasks + +- define request classification inputs +- define the fallback contract to Vite +- define error handling behavior +- define logging behavior for classified requests +- define how direct HTTP requests should appear in logs and diagnostics + +### Deliverable + +An internal dispatcher API that can be tested independently of the full CLI +startup path. + +## Workstream 3: Build the Aggregate Cedar API Runtime for Dev + +### Objective + +Create the aggregate fetch-native backend runtime that the dispatcher will call +for the default Cedar runtime path. + +### Tasks + +- compose GraphQL handling into the aggregate runtime +- compose auth handling into the aggregate runtime +- compose filesystem-discovered function handling into the aggregate runtime +- ensure route matching uses the Phase 2 route manifest or equivalent canonical + route data +- ensure request context enrichment still works correctly +- ensure cookies, params, query, and auth state are available through the new + fetch-native path +- explicitly document that this aggregate runtime covers Cedar-owned backend + surfaces and is not yet a general replacement for arbitrary Fastify plugins or + custom Fastify routes + +### Deliverable + +A single backend fetch dispatcher that can answer all Cedar API requests in dev. + +### Validation Questions + +- Does GraphiQL still load correctly? +- Do auth callback flows still work? +- Do function routes preserve method handling and path params? +- Do direct `curl` requests behave the same as browser-originated requests? + +## Workstream 4: Integrate Backend Execution into the Vite Dev Runtime + +### Objective + +Mount Cedar backend handling into the Vite dev server so one visible host serves +the whole app on the default runtime lane. + +### Tasks + +- install Cedar middleware into the Vite dev server +- intercept and classify requests before SPA fallback handling +- invoke the aggregate Cedar API runtime for backend requests +- fall through to Vite for frontend requests +- ensure Vite internal endpoints are never intercepted incorrectly +- ensure response streaming and headers are preserved correctly where relevant +- ensure this integration is only the default path for standard apps, not a + silent override of custom Fastify server setups + +### Deliverable + +A working one-port dev runtime. + +### Key Acceptance Checks + +- opening the app in the browser works +- GraphQL requests to the visible dev host work +- auth endpoints on the visible dev host work +- function endpoints on the visible dev host work +- HMR still works +- GraphiQL still works + +## Workstream 5: Implement Backend Invalidation and Watch Behavior + +### Objective + +Ensure backend changes are reflected reliably during development. + +### Tasks + +- identify backend-relevant file globs +- hook those changes into Vite-aware invalidation +- invalidate the aggregate backend entry on relevant changes +- ensure generated artifacts that affect backend execution also trigger reload + behavior +- surface backend reload events in logs for debugging + +### Deliverable + +Reliable backend code refresh without manual restarts in normal workflows. + +### Minimum Acceptable Behavior + +If a backend file changes, the next matching request should execute updated code +without requiring the developer to restart `cedar dev`. + +## Workstream 6: Introduce `cedarUniversalDeployPlugin()` in the API Server Vite Build + +### Objective + +Create the first real Cedar UD Vite plugin implementation. + +### Tasks + +- add the plugin to the API server Vite build config +- register `virtual:cedar-api` with UD via `addEntry()` +- resolve `virtual:ud:catch-all` to `virtual:cedar-api` +- emit the virtual module that exports the aggregate Cedar API fetchable +- ensure the plugin only applies in the API server build context +- add the `@cedarjs/api-server` peer dependency to `@cedarjs/vite` + +### Deliverable + +A working plugin that bridges Cedar's aggregate API runtime into UD's Vite entry +model. + +### Important Guardrail + +Do not let this plugin accidentally become coupled to browser build concerns. +Its job in Phase 4 is server-entry registration for the API server build. + +## Workstream 7: Wire `@universal-deploy/node` into the API Server Build and Serve Path + +### Objective + +Make `cedar serve` run the Vite-built Node server entry. + +### Tasks + +- add `node()` from `@universal-deploy/node/vite` to the API server Vite build +- ensure the build output is self-contained enough for `cedar serve` +- update `cedar serve` to launch the built server entry +- remove or bypass the temporary direct `createUDServer`-style path for the Node + serve case +- verify startup, shutdown, logging, and error reporting behavior + +### Deliverable + +`cedar serve` runs the UD Node build output end-to-end. + +### Acceptance Checks + +- `cedar serve` starts successfully from the built output +- GraphQL works +- auth works +- functions work +- direct HTTP requests work +- no Fastify-specific production path is required for this serve mode + +## Workstream 8: CLI and DX Cleanup + +### Objective + +Make the new runtime feel intentional rather than transitional, while making the +compatibility lane explicit for custom-server apps. + +### Tasks + +- update CLI startup messaging to show one visible port for the default runtime +- remove or reduce references to separate web/API dev ports in normal output for + standard apps +- update any generated URLs, docs, or help text that assume proxying +- ensure error messages mention the unified host where appropriate +- ensure debugging output still makes it clear whether a request was handled by + Vite web logic or Cedar backend logic +- add explicit messaging for custom-server apps so users understand when Cedar + is using the compatibility lane instead of the default unified runtime + +### Deliverable + +A coherent developer experience that matches the new architecture. + +## Implementation Sequence + +A practical implementation order is: + +### Step 1: Runtime Mapping + +Complete Workstream 1 and document the current orchestration points. + +### Step 2: Dispatcher Contract + +Define and implement the internal dev request dispatcher contract. + +### Step 3: Aggregate Backend Runtime + +Build the aggregate Cedar API fetch dispatcher and validate it outside the full +Vite integration if possible. + +### Step 4: Vite Dev Integration + +Mount the dispatcher into the Vite dev server and get one-port request handling +working. + +### Step 5: Invalidation + +Add backend file invalidation and reload behavior. + +### Step 6: UD Plugin + +Introduce `cedarUniversalDeployPlugin()` in the API server Vite build. + +### Step 7: Node Serve Integration + +Add `node()` and switch `cedar serve` to the built server entry. + +### Step 8: DX Cleanup and Documentation + +Update CLI messaging, docs, and migration notes. + +This order reduces risk by proving the runtime model before tightening the build +and serve integration. + +## Testing Strategy + +## 1. Unit-Level Testing + +Test the request dispatcher in isolation. + +### Cases + +- Vite internal request is passed through +- GraphQL request is routed to backend runtime +- auth request is routed to backend runtime +- function request is routed to backend runtime +- SPA/document request falls through to web handling +- unknown request gets the correct fallback behavior + +## 2. Integration Testing for Dev Runtime + +Test the unified dev host end-to-end for the default runtime lane. + +### Cases + +- browser loads app from one port +- GraphQL POST works against same host +- GraphiQL loads from same host +- auth callback route works against same host +- function route works against same host +- static assets still load +- HMR still functions after frontend edits +- backend code changes are reflected on next request + +## 3. Serve-Path Testing + +Test the Vite-built Node server output for the default runtime lane. + +### Cases + +- `cedar serve` starts from built output +- GraphQL works +- auth works +- functions work +- route params and query parsing work +- cookies and headers are preserved correctly + +## 4. Regression Testing + +Focus on areas most likely to break: + +- auth providers with callback flows +- GraphiQL tooling +- function routes with non-GET methods +- middleware ordering +- generated route manifest changes +- direct `curl` requests without browser headers +- custom-server apps that use `api/src/server.{ts,js}` +- Fastify plugin registration and custom Fastify routes on the compatibility + lane + +## Suggested Milestones + +## Milestone A: Aggregate Backend Runtime Works + +Success means: + +- one aggregate fetch dispatcher exists +- GraphQL, auth, and functions all work through it +- it can be invoked independently of the final Vite integration + +## Milestone B: One-Port Dev Host Works + +Success means: + +- browser, GraphQL, auth, and functions all work from one visible host +- Vite HMR still works +- no separate backend port is required for normal use + +## Milestone C: Backend Reload Works Reliably + +Success means: + +- backend edits are reflected without manual restart +- stale module behavior is not observed in normal workflows + +## Milestone D: `cedar serve` Uses UD Node Output + +Success means: + +- API server Vite build emits the server entry +- `cedar serve` launches it successfully +- the temporary direct server path is no longer needed for the Node serve case + +## Risks and Mitigations + +## Risk 1: Vite Internal Requests Are Misclassified + +### Impact + +HMR or module loading breaks in confusing ways. + +### Mitigation + +- classify Vite internal requests first +- add explicit tests for Vite-specific paths +- add debug logging around request classification during development + +## Risk 2: Backend Module Invalidation Is Incomplete + +### Impact + +Developers see stale backend behavior and lose trust in the runtime. + +### Mitigation + +- start with coarse invalidation at the aggregate entry boundary +- prefer correctness over fine-grained optimisation +- log backend invalidation events during early rollout + +## Risk 3: Auth Flows Regress + +### Impact + +Login/logout/callback behavior breaks, often only in certain providers. + +### Mitigation + +- explicitly test callback-style providers +- test cookie-based and token-based auth paths +- preserve existing request context enrichment semantics + +## Risk 4: GraphiQL or Direct HTTP Tooling Regresses + +### Impact + +Developer workflows become worse even if browser flows work. + +### Mitigation + +- treat GraphiQL and `curl` as first-class acceptance cases +- test non-browser requests explicitly +- avoid assumptions that all requests originate from the SPA + +## Risk 5: Phase 4 Accidentally Expands into Phase 5 + +### Impact + +Delivery slows down because the team tries to solve per-route provider +registration too early. + +### Mitigation + +- keep the aggregate-entry boundary explicit +- defer per-route UD registration to Phase 5 +- document temporary limitations clearly + +## Risk 6: Terminology Confusion Around "SSR" + +### Impact + +Future maintainers wire `node()` into the wrong Vite config or conflate API +server builds with Cedar HTML SSR. + +### Mitigation + +- document the distinction repeatedly +- use precise naming in config and comments +- avoid ambiguous labels like "SSR build" without qualification + +## Risk 7: Custom Fastify Behaviour Is Silently Lost + +### Impact + +Apps that rely on `api/src/server.{ts,js}`, `configureFastify`, +`configureApiServer`, or direct Fastify plugin registration appear to start, but +some custom routes, hooks, parsers, decorators, or request/reply behaviour stop +working. + +### Mitigation + +- preserve an explicit compatibility lane for custom-server apps +- detect custom-server usage and branch intentionally +- never silently route custom-server apps through the default unified runtime if + that would drop supported behaviour +- document the boundary between Cedar-owned fetch-native runtime support and + Fastify-specific compatibility support + +## Open Design Questions to Resolve During Implementation + +These do not block writing the plan, but they should be resolved early in +implementation: + +1. What is the exact internal API between the dispatcher and Vite fallback + handling? +2. Which backend file changes should trigger aggregate invalidation directly, and + which should rely on dependency tracking? +3. How should backend runtime errors be surfaced in dev: + - terminal only + - HTTP response only + - both +4. Should the aggregate backend runtime be lazily initialised on first request + or eagerly prepared at dev startup? +5. Are there any auth providers that currently depend on assumptions about a + separate backend origin? +6. Does GraphiQL require any path or asset handling adjustments when moved fully + behind the unified host? +7. What is the cleanest migration path for any existing CLI flags or docs that + expose separate dev ports? +8. What is the exact runtime-selection rule for deciding when an app stays on + the custom Fastify compatibility lane? +9. Should custom-server apps keep the current split dev model in Phase 4, or is + there a safe compatibility wrapper that still preserves Fastify behaviour? +10. Which current Fastify extension points need a future framework-agnostic + replacement, and which should remain explicitly serverful-only? + +## Exit Criteria for Phase 4 + +Phase 4 should be considered complete when all of the following are true: + +- `cedar dev` exposes one externally visible host/port for the default runtime + path +- GraphQL requests work directly against that host on the default runtime path +- auth requests work directly against that host on the default runtime path +- function requests work directly against that host on the default runtime path +- browser app loading and HMR still work on the default runtime path +- backend code changes are reflected without manual restart in normal workflows + on the default runtime path +- the API server Vite build includes `cedarUniversalDeployPlugin()` +- the API server Vite build includes `node()` from + `@universal-deploy/node/vite` +- `cedar serve` runs the Vite-built Node server entry for the default runtime + path +- Cedar no longer depends on a separately exposed backend port for the standard + dev experience +- custom-server apps still have a documented and supported compatibility path +- the implementation does not require Cedar HTML SSR/RSC work to be complete + +## Deliverables + +Phase 4 should produce the following concrete outputs: + +- unified one-port dev runtime for the default Cedar runtime lane +- internal dev request dispatcher +- aggregate Cedar API fetch dispatcher for dev +- backend invalidation/reload behavior integrated with the Vite-centric runtime +- initial `cedarUniversalDeployPlugin()` in `@cedarjs/vite` +- `@cedarjs/api-server` peer dependency declared by `@cedarjs/vite` +- API server Vite build wired with `node()` from `@universal-deploy/node/vite` +- `cedar serve` updated to run the built Node server entry for the default + runtime lane +- documented compatibility lane for apps using custom Fastify server setup +- updated docs and CLI messaging reflecting both the unified runtime and the + compatibility lane + +## Recommendation + +Implement Phase 4 as a runtime unification phase, not as a provider-expansion +phase. + +The most important outcome is that Cedar development becomes operationally +single-host and architecturally Vite-centric on the default runtime lane, while +`cedar serve` moves onto the UD Node build output for that same lane. If that is +achieved with an aggregate API entry and conservative backend invalidation, +Phase 4 is successful. + +That success condition should not require Cedar to immediately eliminate the +custom Fastify server path. Existing apps that depend on `api/src/server.{ts,js}` +or Fastify-specific plugin setup should remain supported through an explicit +compatibility lane. Phase 5 and later work can then build on a stable default +runtime foundation while separately addressing longer-term migration away from +Fastify-specific extension points where appropriate. +conservative backend invalidation, Phase 4 is successful. + +Phase 5 can then build on a stable runtime foundation to formalise per-route UD +entry registration and provider-facing metadata correctness. diff --git a/packages/api-server/dist.test.ts b/packages/api-server/dist.test.ts index 4916639137..55d2102f0a 100644 --- a/packages/api-server/dist.test.ts +++ b/packages/api-server/dist.test.ts @@ -11,7 +11,7 @@ describe('dist', () => { expect(fs.existsSync(path.join(distPath, '__tests__'))).toEqual(false) }) - it('ships three bins', () => { + it('ships six bins', () => { expect(packageConfig.bin).toMatchInlineSnapshot(` { "cedarjs-api-server-watch": "./dist/watch.js", diff --git a/packages/api-server/package.json b/packages/api-server/package.json index 7ff79892c4..7966a26e23 100644 --- a/packages/api-server/package.json +++ b/packages/api-server/package.json @@ -73,6 +73,18 @@ "types": "./dist/cjs/bothCLIConfigHandler.d.ts", "default": "./dist/cjs/bothCLIConfigHandler.js" }, + "./udDispatcher": { + "import": { + "types": "./dist/udDispatcher.d.ts", + "default": "./dist/udDispatcher.js" + } + }, + "./udFetchable": { + "import": { + "types": "./dist/udFetchable.d.ts", + "default": "./dist/udFetchable.js" + } + }, "./watch": { "import": { "types": "./dist/watch.d.ts", @@ -118,6 +130,8 @@ "@cedarjs/web-server": "workspace:*", "@fastify/multipart": "9.4.0", "@fastify/url-data": "6.0.3", + "@universal-deploy/node": "^0.1.6", + "@universal-deploy/store": "^0.2.1", "ansis": "4.2.0", "chokidar": "3.6.0", "dotenv-defaults": "5.0.2", @@ -128,7 +142,9 @@ "picoquery": "2.5.0", "pretty-bytes": "5.6.0", "pretty-ms": "7.0.1", + "rou3": "^0.8.1", "split2": "4.2.0", + "srvx": "^0.11.9", "yargs": "17.7.2" }, "devDependencies": { diff --git a/packages/api-server/src/__tests__/udFetchable.test.ts b/packages/api-server/src/__tests__/udFetchable.test.ts new file mode 100644 index 0000000000..0641bf8c67 --- /dev/null +++ b/packages/api-server/src/__tests__/udFetchable.test.ts @@ -0,0 +1,110 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +import type { CedarHandler, CedarRequestContext } from '@cedarjs/api/runtime' +import { buildCedarContext } from '@cedarjs/api/runtime' + +import { createCedarFetchable } from '../udFetchable.js' + +vi.mock('@cedarjs/api/runtime', async (importOriginal) => { + const actual = (await importOriginal()) as any + return { + ...actual, + buildCedarContext: vi.fn().mockImplementation(actual.buildCedarContext), + } +}) + +afterEach(() => { + vi.clearAllMocks() +}) + +describe('createCedarFetchable', () => { + describe('wraps a CedarHandler', () => { + it('calls buildCedarContext and the handler, and returns the handler Response', async () => { + let capturedCtx: CedarRequestContext | undefined + + const handler: CedarHandler = async (_req, ctx) => { + capturedCtx = ctx + return new Response('ok', { status: 200 }) + } + + const fetchable = createCedarFetchable(handler) + const request = new Request('http://localhost/test') + + const response = await fetchable.fetch(request) + + expect(buildCedarContext).toHaveBeenCalledWith(request) + expect(capturedCtx).toBeDefined() + expect(response.status).toBe(200) + }) + + it('returns the Response from the handler', async () => { + const handler: CedarHandler = async () => { + return new Response('hello world', { + status: 201, + headers: { 'x-custom': 'value' }, + }) + } + + const fetchable = createCedarFetchable(handler) + const response = await fetchable.fetch( + new Request('http://localhost/test'), + ) + + expect(response.status).toBe(201) + expect(response.headers.get('x-custom')).toBe('value') + expect(await response.text()).toBe('hello world') + }) + }) + + describe('passes the correct context to the handler', () => { + it('passes query params from the URL', async () => { + let capturedCtx: CedarRequestContext | undefined + + const handler: CedarHandler = async (_req, ctx) => { + capturedCtx = ctx + return new Response('ok') + } + + const fetchable = createCedarFetchable(handler) + await fetchable.fetch( + new Request('http://localhost/test?name=cedar&version=1'), + ) + + expect(capturedCtx?.query.get('name')).toBe('cedar') + expect(capturedCtx?.query.get('version')).toBe('1') + }) + + it('passes cookies from request headers', async () => { + let capturedCtx: CedarRequestContext | undefined + + const handler: CedarHandler = async (_req, ctx) => { + capturedCtx = ctx + return new Response('ok') + } + + const fetchable = createCedarFetchable(handler) + await fetchable.fetch( + new Request('http://localhost/test', { + headers: { cookie: 'session=abc123; theme=dark' }, + }), + ) + + expect(capturedCtx?.cookies.get('session')).toBe('abc123') + expect(capturedCtx?.cookies.get('theme')).toBe('dark') + }) + + it('has empty params by default (no route params injected)', async () => { + let capturedCtx: CedarRequestContext | undefined + + const handler: CedarHandler = async (_req, ctx) => { + capturedCtx = ctx + return new Response('ok') + } + + const fetchable = createCedarFetchable(handler) + await fetchable.fetch(new Request('http://localhost/test')) + + expect(capturedCtx?.params).toEqual({}) + }) + }) +}) diff --git a/packages/api-server/src/udDispatcher.ts b/packages/api-server/src/udDispatcher.ts new file mode 100644 index 0000000000..dae60fbafd --- /dev/null +++ b/packages/api-server/src/udDispatcher.ts @@ -0,0 +1,288 @@ +import path from 'node:path' +import { pathToFileURL } from 'node:url' + +import type { EntryMeta } from '@universal-deploy/store' +import fg from 'fast-glob' +import { addRoute, createRouter, findRoute } from 'rou3' + +import type { CedarHandler } from '@cedarjs/api/runtime' +import { buildCedarContext, requestToLegacyEvent } from '@cedarjs/api/runtime' +import type { GlobalContext } from '@cedarjs/context' +import { getAsyncStoreInstance } from '@cedarjs/context/dist/store' +import { getPaths } from '@cedarjs/project-config' + +import type { Fetchable } from './udFetchable.js' +import { createCedarFetchable } from './udFetchable.js' + +const ALL_HTTP_METHODS = [ + 'GET', + 'HEAD', + 'POST', + 'PUT', + 'DELETE', + 'PATCH', + 'OPTIONS', + 'CONNECT', + 'TRACE', +] as const +const GRAPHQL_METHODS = ['GET', 'POST', 'OPTIONS'] as const + +export interface CedarDispatcherOptions { + apiRootPath?: string + discoverFunctionsGlob?: string | string[] +} + +export interface CedarDispatcherResult { + fetchable: Fetchable + registrations: EntryMeta[] +} + +/** + * Normalizes the api root path so it always starts and ends with a `/`. + * e.g. `v1` → `/v1/`, `/v1` → `/v1/`, `/` → `/` + */ +function normalizeApiRootPath(rootPath: string): string { + let normalized = rootPath + + if (!normalized.startsWith('/')) { + normalized = '/' + normalized + } + + if (!normalized.endsWith('/')) { + normalized = normalized + '/' + } + + return normalized +} + +// NOTE: The runtime function-discovery approach used here (scanning +// `api/dist/functions/` with fast-glob at startup) is now used by two +// callers: +// 1. `cedarUniversalDeployPlugin` — the Phase 4 Vite plugin that registers +// `virtual:cedar-api` in the API server Vite build +// 2. `cedarDevDispatcherPlugin` — the Phase 4 Vite dev middleware that +// serves API requests from the unified Vite dev host +// +// TODO: Once the API server is fully Vite-built and Phase 5 introduces +// per-route static entry registration, the fast-glob discovery path can be +// removed in favour of statically-known routes. Until then, this function +// remains the shared aggregate dispatcher for both callers. +/** + * Shared aggregate Cedar API dispatcher used by + * `cedarUniversalDeployPlugin` (via `virtual:cedar-api`) and + * `cedarDevDispatcherPlugin` (the unified Vite dev host middleware). + * + * Discovers Cedar API functions in `api/dist/functions/`, builds a rou3 router + * and a map of route names to Fetchables, then returns a single Fetchable that + * routes incoming Fetch-API requests to the correct per-function handler. + * Also returns the list of `EntryMeta` registrations so callers can forward + * them to `@universal-deploy/store` via `addEntry()`. + */ +export async function buildCedarDispatcher( + options?: CedarDispatcherOptions, +): Promise { + const normalizedApiRootPath = normalizeApiRootPath( + options?.apiRootPath ?? '/', + ) + const discoverFunctionsGlob = + options?.discoverFunctionsGlob ?? 'dist/functions/**/*.{ts,js}' + + // Discover function files in api/dist/functions/ + // deep: 2 is intentional: with cwd=api/, depth 1 is dist/ and depth 2 is + // dist/functions/, so one level of subdirectory nesting below + // dist/functions/ (e.g. dist/functions/nested/nested.js) is supported but + // deeper nesting is not. This matches the behaviour of the Fastify-based + // lambdaLoader and @cedarjs/internal's findApiDistFunctions, which carry + // the same deep: 2 limit with the explicit note "We don't support deeply + // nested api functions, to maximise compatibility with deployment providers". + // See packages/internal/src/files.ts + const serverFunctions = fg.sync(discoverFunctionsGlob, { + cwd: getPaths().api.base, + deep: 2, + absolute: true, + }) + + // Put the graphql function first for consistent load ordering + const graphqlIdx = serverFunctions.findIndex( + (x) => path.basename(x, path.extname(x)) === 'graphql', + ) + + if (graphqlIdx >= 0) { + const [graphqlFn] = serverFunctions.splice(graphqlIdx, 1) + serverFunctions.unshift(graphqlFn) + } + + // Build fetchable map: routeName -> Fetchable + const fetchableMap = new Map() + + // Build rou3 router for URL pattern matching + const router = createRouter() + + const registrations: EntryMeta[] = [] + + for (const fnPath of serverFunctions) { + const routeName = path.basename(fnPath, path.extname(fnPath)) + const routePath = routeName === 'graphql' ? '/graphql' : `/${routeName}` + + const fnImport = await import(pathToFileURL(fnPath).href) + + // Check if this is a GraphQL function — the babel plugin adds + // `__rw_graphqlOptions` to api/dist/functions/graphql.js + if ( + '__rw_graphqlOptions' in fnImport && + fnImport.__rw_graphqlOptions != null + ) { + const { createGraphQLYoga } = await import('@cedarjs/graphql-server') + + // Cast through unknown to bridge the CJS/ESM module resolution type + // mismatch: the static import resolves to CJS types in a CJS build, while + // the dynamic import always resolves to ESM types. Deriving the type from + // createGraphQLYoga itself guarantees both sides use the same resolution. + const graphqlOptions = + fnImport.__rw_graphqlOptions as unknown as Parameters< + typeof createGraphQLYoga + >[0] + + const { yoga } = createGraphQLYoga(graphqlOptions) + + const graphqlFetchable: Fetchable = { + async fetch(request: Request): Promise { + const cedarContext = await buildCedarContext(request, { + authDecoder: graphqlOptions.authDecoder, + }) + const event = await requestToLegacyEvent(request, cedarContext) + + // Phase 1 transitional context bridge: pass both Fetch-native fields + // (request, cedarContext) and legacy bridge fields (event, + // requestContext) so that Cedar-owned Yoga plugins that have not yet + // migrated to the Fetch-native shape continue to work. + return yoga.handle(request, { + request, + cedarContext, + event, + requestContext: undefined, + }) + }, + } + + fetchableMap.set(routeName, graphqlFetchable) + + registrations.push({ + id: routePath, + route: routePath, + method: [...GRAPHQL_METHODS], + }) + + for (const method of GRAPHQL_METHODS) { + addRoute(router, method, routePath, routeName) + addRoute(router, method, `${routePath}/**`, routeName) + } + + // Skip regular handler processing for the graphql function + continue + } + + // Only Fetch-native handlers are supported by the Universal Deploy server. + // Functions that export only a legacy Lambda-shaped `handler` are not + // WinterTC-compatible and must be migrated to `export async function + // handle(request, ctx)` before they can be served by this runtime. + const cedarHandler: CedarHandler | undefined = (() => { + if ('handle' in fnImport && typeof fnImport.handle === 'function') { + return fnImport.handle as CedarHandler + } + + if ( + 'default' in fnImport && + fnImport.default != null && + 'handle' in fnImport.default && + typeof fnImport.default.handle === 'function' + ) { + return fnImport.default.handle as CedarHandler + } + + return undefined + })() + + if (!cedarHandler) { + console.warn( + routeName, + 'at', + fnPath, + 'does not export a Fetch-native `handle` function and will not be' + + ' served by the Universal Deploy server. Migrate to' + + ' `export async function handle(request, ctx)` or use' + + ' `yarn cedar serve` for legacy Lambda-shaped handler support.', + ) + continue + } + + const handler = cedarHandler + + fetchableMap.set(routeName, createCedarFetchable(handler)) + + registrations.push({ + id: routePath, + route: routePath, + // method omitted → matches all HTTP methods per @universal-deploy/store docs + }) + + for (const method of ALL_HTTP_METHODS) { + addRoute(router, method, routePath, routeName) + addRoute(router, method, `${routePath}/**`, routeName) + } + } + + const fetchable: Fetchable = { + fetch(request: Request): Promise { + return getAsyncStoreInstance().run( + new Map(), + async () => { + const url = new URL(request.url) + let routePathname = url.pathname + + // Strip the apiRootPath prefix so that `/api/hello` becomes `/hello` + if ( + normalizedApiRootPath !== '/' && + routePathname.startsWith(normalizedApiRootPath) + ) { + // normalizedApiRootPath ends with '/', so slice length - 1 to keep + // the leading slash on the remaining path segment + routePathname = routePathname.slice( + normalizedApiRootPath.length - 1, + ) + } + + if (!routePathname.startsWith('/')) { + routePathname = '/' + routePathname + } + + const match = findRoute(router, request.method, routePathname) + + if (!match) { + return new Response('Not Found', { status: 404 }) + } + + const matchedRouteName = match.data + const fnFetchable = fetchableMap.get(matchedRouteName) + + if (!fnFetchable) { + return new Response('Not Found', { status: 404 }) + } + + try { + return await fnFetchable.fetch(request) + } catch (err) { + console.error( + 'Unhandled error in fetch handler for route', + matchedRouteName, + err, + ) + return new Response('Internal Server Error', { status: 500 }) + } + }, + ) + }, + } + + return { fetchable, registrations } +} diff --git a/packages/api-server/src/udFetchable.ts b/packages/api-server/src/udFetchable.ts new file mode 100644 index 0000000000..b3762fccd9 --- /dev/null +++ b/packages/api-server/src/udFetchable.ts @@ -0,0 +1,21 @@ +import type { CedarHandler } from '@cedarjs/api/runtime' +import { buildCedarContext } from '@cedarjs/api/runtime' + +export interface Fetchable { + fetch(request: Request): Response | Promise +} + +/** + * Wraps a CedarHandler in a WinterTC-compatible Fetchable. + * + * The Fetchable calls buildCedarContext to produce a CedarRequestContext, + * then delegates to the handler. + */ +export function createCedarFetchable(handler: CedarHandler): Fetchable { + return { + async fetch(request: Request): Promise { + const ctx = await buildCedarContext(request) + return handler(request, ctx) + }, + } +} diff --git a/packages/cli/src/commands/build/buildHandler.ts b/packages/cli/src/commands/build/buildHandler.ts index 35f5445150..709fbe89e8 100644 --- a/packages/cli/src/commands/build/buildHandler.ts +++ b/packages/cli/src/commands/build/buildHandler.ts @@ -20,6 +20,7 @@ import { loadAndValidateSdls } from '@cedarjs/internal/dist/validateSchema' import { detectPrerenderRoutes } from '@cedarjs/prerender/detection' import { type Paths } from '@cedarjs/project-config' import { timedTelemetry } from '@cedarjs/telemetry' +import { buildUDApiServer } from '@cedarjs/vite/buildUDApiServer' import { generatePrismaCommand } from '../../lib/generatePrismaClient.js' // @ts-expect-error - Types not available for JS files @@ -233,6 +234,11 @@ export const handler = async ({ title: 'Verifying graphql schema...', task: loadAndValidateSdls, }, + // The API build has two sequential steps: + // 1. esbuild compiles api/src/** → api/dist/ (functions, services, etc.) + // 2. Vite wraps api/dist/functions/ into a self-contained UD Node server + // entry at api/dist/ud/index.js for `cedar serve api` + // Step 2 depends on step 1 having completed. workspace.includes('api') && { title: 'Building API...', task: async () => { @@ -247,6 +253,12 @@ export const handler = async ({ } }, }, + workspace.includes('api') && { + title: 'Bundling API server entry (Universal Deploy)...', + task: async () => { + await buildUDApiServer({ verbose }) + }, + }, workspace.includes('web') && { title: 'Building Web...', task: async () => { diff --git a/packages/cli/src/commands/dev/devHandler.ts b/packages/cli/src/commands/dev/devHandler.ts index 975681b6ab..ced14f35db 100644 --- a/packages/cli/src/commands/dev/devHandler.ts +++ b/packages/cli/src/commands/dev/devHandler.ts @@ -46,23 +46,12 @@ export const handler = async ({ const serverFile = serverFileExists() - // Starting values of ports from config (cedar.toml or redwood.toml) - const apiPreferredPort = parseInt(String(getConfig().api.port)) - - let webPreferredPort: number | undefined = parseInt( - String(getConfig().web.port), - ) - - // Assume we can have the ports we want - let apiAvailablePort = apiPreferredPort - let apiPortChangeNeeded = false - let webAvailablePort = webPreferredPort - let webPortChangeNeeded = false - - // Check api port, unless there's a serverFile. If there is a serverFile, we - // don't know what port will end up being used in the end. It's up to the - // author of the server file to decide and handle that - if (workspace.includes('api') && !serverFile) { + // For the custom-server lane (apps with api/src/server.{ts,js}), we still + // need to find a free API port since the server file controls its own + // listening and we don't know what port it will use. + let apiAvailablePort: number | undefined + if (workspace.includes('api') && serverFile) { + const apiPreferredPort = parseInt(String(getConfig().api.port)) apiAvailablePort = await getFreePort(apiPreferredPort) if (apiAvailablePort === -1) { @@ -70,10 +59,14 @@ export const handler = async ({ message: `Could not determine a free port for the api server`, }) } - - apiPortChangeNeeded = apiAvailablePort !== apiPreferredPort } + let webPreferredPort: number | undefined = parseInt( + String(getConfig().web.port), + ) + let webAvailablePort = webPreferredPort + let webPortChangeNeeded = false + // Check web port if (workspace.includes('web')) { // Extract any ports the user forwarded to the dev server and prefer that @@ -87,10 +80,7 @@ export const handler = async ({ webPreferredPort = port ? parseInt(port, 10) : undefined } - webAvailablePort = await getFreePort(webPreferredPort, [ - apiPreferredPort, - apiAvailablePort, - ]) + webAvailablePort = await getFreePort(webPreferredPort) if (webAvailablePort === -1) { exitWithError(undefined, { @@ -102,19 +92,16 @@ export const handler = async ({ } // Check for port conflict and exit with message if found - if (apiPortChangeNeeded || webPortChangeNeeded) { + if (webPortChangeNeeded) { const message = [ - 'The currently configured ports for the development server are', - 'unavailable. Suggested changes to your ports, which can be changed in', - 'cedar.toml (or redwood.toml), are:\n', - apiPortChangeNeeded && ` - API to use port ${apiAvailablePort} instead`, - apiPortChangeNeeded && 'of your currently configured', - apiPortChangeNeeded && `${apiPreferredPort}\n`, - webPortChangeNeeded && ` - Web to use port ${webAvailablePort} instead`, - webPortChangeNeeded && 'of your currently configured', - webPortChangeNeeded && `${webPreferredPort}\n`, - '\nCannot run the development server until your configured ports are', - 'changed or become available.', + 'The currently configured port for the development server is', + 'unavailable. Suggested change to your port, which can be changed in', + 'cedar.toml (or redwood.toml):\n', + ` - Web to use port ${webAvailablePort} instead`, + 'of your currently configured', + `${webPreferredPort}\n`, + '\nCannot run the development server until your configured port is', + 'changed or becomes available.', ] .filter(Boolean) .join(' ') @@ -130,23 +117,9 @@ export const handler = async ({ errorTelemetry(process.argv, `Error generating prisma client: ${message}`) console.error(c.error(message)) } - - // Again, if a server file is configured, we don't know what port it'll end - // up using - if (!serverFile) { - try { - await shutdownPort(apiAvailablePort) - } catch (e) { - const message = getErrorMessage(e) - errorTelemetry(process.argv, `Error shutting down "api": ${message}`) - console.error( - `Error whilst shutting down "api" port: ${c.error(message)}`, - ) - } - } } - if (workspace.includes('web')) { + if (workspace.includes('web') && webAvailablePort !== undefined) { try { await shutdownPort(webAvailablePort) } catch (e) { @@ -209,7 +182,13 @@ export const handler = async ({ runWhen?: () => boolean })[] = [] - if (workspace.includes('api')) { + // For the custom-server compatibility lane (apps with api/src/server.{ts,js}), + // we still start a separate API process because those apps use Fastify-specific + // extension points (configureApiServer, direct plugin registration, etc.) that + // are not yet covered by the unified Vite dev runtime. For standard Cedar apps + // the cedarDevDispatcherPlugin handles API requests directly inside the Vite + // dev server — no separate API process is needed. + if (workspace.includes('api') && serverFile) { jobs.push({ name: 'api', command: [ @@ -218,7 +197,7 @@ export const handler = async ({ ` --watch "${cedarConfigPath}"`, ` --exec "yarn ${serverWatchCommand}`, ` --port ${apiAvailablePort}`, - ` ${getApiDebugFlag(apiDebugPort, apiAvailablePort)}`, + ` ${getApiDebugFlag(apiDebugPort, apiAvailablePort!)}`, ' | rw-log-formatter"', ] .join(' ') diff --git a/packages/cli/src/commands/serve.ts b/packages/cli/src/commands/serve.ts index 7c9f406e4d..bf4585003b 100644 --- a/packages/cli/src/commands/serve.ts +++ b/packages/cli/src/commands/serve.ts @@ -1,5 +1,6 @@ +import { fork } from 'node:child_process' import fs from 'node:fs' -import path from 'path' +import path from 'node:path' import { terminalLink } from 'termi-link' import type { Argv } from 'yargs' @@ -27,6 +28,7 @@ type ServeArgv = Record & { socket?: string apiRootPath?: string apiHost?: string + ud?: boolean } export const builder = async (yargs: Argv) => { @@ -69,7 +71,18 @@ export const builder = async (yargs: Argv) => { .command({ command: 'api', description: apiServerCLIConfig.description, - builder: apiServerCLIConfig.builder, + builder: (yargs: Argv) => { + if (typeof apiServerCLIConfig.builder === 'function') { + apiServerCLIConfig.builder(yargs) + } + return yargs.option('ud', { + // UD serving is the default. Pass --no-ud to use the legacy Fastify server instead. + description: + 'Use the Universal Deploy server (srvx). This is the default; pass --no-ud to use Fastify instead.', + type: 'boolean', + default: true, + }) + }, handler: async (argv: ServeArgv) => { recordTelemetryAttributes({ command: 'serve', @@ -79,6 +92,58 @@ export const builder = async (yargs: Argv) => { apiRootPath: argv.apiRootPath, }) + if (argv.ud) { + // Launch the Vite-built Universal Deploy Node server entry produced + // by `cedar build api`. The entry at api/dist/ud/index.js is a + // self-contained srvx server that imports virtual:ud:catch-all, + // resolved by cedarUniversalDeployPlugin to Cedar's aggregate fetch + // dispatcher. + const udEntryPath = path.join(getPaths().api.dist, 'ud', 'index.js') + + if (!fs.existsSync(udEntryPath)) { + console.error( + c.error( + `\n Universal Deploy server entry not found at ${udEntryPath}.\n` + + ' Please run `yarn cedar build api` before serving.\n', + ), + ) + process.exit(1) + } + + const udArgs: string[] = [] + + if (argv.port) { + udArgs.push('--port', String(argv.port)) + } + + if (argv.host) { + udArgs.push('--host', argv.host) + } + + await new Promise((resolve, reject) => { + const child = fork(udEntryPath, udArgs, { + execArgv: process.execArgv, + env: { + ...process.env, + NODE_ENV: process.env.NODE_ENV ?? 'production', + PORT: argv.port ? String(argv.port) : process.env.PORT, + HOST: argv.host ?? process.env.HOST, + }, + }) + + child.on('error', reject) + child.on('exit', (code) => { + if (code !== 0) { + reject(new Error(`UD server exited with code ${code}`)) + } else { + resolve() + } + }) + }) + + return + } + // Run the server file, if it exists, api side only if (serverFileExists()) { const { apiServerFileHandler } = await import('./serveApiHandler.js') diff --git a/packages/vite/package.json b/packages/vite/package.json index e370c2786b..7b9bfe3508 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -33,6 +33,9 @@ "require": "./dist/cjs/buildFeServer.js", "import": "./dist/buildFeServer.js" }, + "./buildUDApiServer": { + "import": "./dist/buildUDApiServer.js" + }, "./build": { "import": "./dist/build/build.js", "default": "./dist/cjs/build/build.js" @@ -73,6 +76,7 @@ "@cedarjs/testing": "workspace:*", "@cedarjs/web": "workspace:*", "@swc/core": "1.15.24", + "@universal-deploy/node": "^0.1.6", "@vitejs/plugin-react": "4.7.0", "@whatwg-node/fetch": "0.10.13", "@whatwg-node/server": "0.10.18", @@ -112,6 +116,14 @@ "typescript": "5.9.3", "vitest": "3.2.4" }, + "peerDependencies": { + "@cedarjs/api-server": "workspace:*" + }, + "peerDependenciesMeta": { + "@cedarjs/api-server": { + "optional": false + } + }, "engines": { "node": ">=24" }, diff --git a/packages/vite/src/buildUDApiServer.ts b/packages/vite/src/buildUDApiServer.ts new file mode 100644 index 0000000000..187becddf3 --- /dev/null +++ b/packages/vite/src/buildUDApiServer.ts @@ -0,0 +1,94 @@ +import path from 'node:path' + +import { getPaths } from '@cedarjs/project-config' + +export interface BuildUDApiServerOptions { + verbose?: boolean + apiRootPath?: string +} + +/** + * Builds the API server Universal Deploy Node entry using Vite. + * + * Runs a Vite server build that: + * 1. Installs `cedarUniversalDeployPlugin()` to register `virtual:cedar-api` + * and resolve `virtual:ud:catch-all` → Cedar's aggregate fetch dispatcher + * 2. Installs `node()` from `@universal-deploy/node/vite` to emit a + * self-contained Node server entry at `api/dist/ud/index.js` + * + * The emitted entry can be launched directly: node api/dist/ud/index.js + * That is what `cedar serve api` does. + * + * NOTE: The Vite "ssr" build target used here is a server-side module build + * concern — it is NOT related to Cedar HTML SSR or RSC. "ssr" simply means + * Vite produces a Node-compatible bundle rather than a browser bundle. + */ +export const buildUDApiServer = async ({ + verbose = false, + apiRootPath, +}: BuildUDApiServerOptions = {}) => { + // Dynamic imports so that vite and the UD plugins are only loaded when + // this function is actually called (keeps cold-start cost of importing + // @cedarjs/vite low for callers that only need the web build path). + const { build } = await import('vite') + const { cedarUniversalDeployPlugin } = + await import('./plugins/vite-plugin-cedar-universal-deploy.js') + const { node } = await import('@universal-deploy/node/vite') + + const rwPaths = getPaths() + + // The UD Node server entry is placed under api/dist/ud/ so it does not + // collide with the existing esbuild output under api/dist/. + const outDir = path.join(rwPaths.api.dist, 'ud') + + await build({ + // No configFile — we configure everything inline so this build is + // self-contained and does not require a vite.config.ts in api/. + configFile: false, + envFile: false, + logLevel: verbose ? 'info' : 'warn', + + plugins: [ + // Registers virtual:cedar-api with @universal-deploy/store and resolves + // virtual:ud:catch-all → virtual:cedar-api → Cedar's aggregate fetchable. + cedarUniversalDeployPlugin({ apiRootPath }), + + // Emits a self-contained Node server entry (api/dist/ud/index.js) that + // imports virtual:ud:catch-all and starts an srvx HTTP server. + // This is a Vite server-build concern, not Cedar HTML SSR. + ...node(), + ], + + // The ssr environment is the Vite mechanism for server-side builds. + // Reminder: "ssr" here means "server-side module execution", NOT + // Cedar HTML SSR / streaming / RSC. + environments: { + ssr: { + build: { + outDir, + // Ensure @universal-deploy/node is bundled into the output so the + // emitted entry is self-contained. + rollupOptions: { + output: { + // Produce a single-file entry where possible; srvx chunks are + // split by the node() plugin automatically. + entryFileNames: '[name].js', + }, + }, + }, + resolve: { + // Do not externalise @universal-deploy/node — the node() plugin + // requires it to be bundled into the server entry. + noExternal: ['@universal-deploy/node'], + }, + }, + }, + + build: { + // Write the server entry to api/dist/ud/ + outDir, + // This is a server (Node) build, not a browser build. + ssr: true, + }, + }) +} diff --git a/packages/vite/src/index.ts b/packages/vite/src/index.ts index 243287b4c9..8c20969d57 100644 --- a/packages/vite/src/index.ts +++ b/packages/vite/src/index.ts @@ -10,11 +10,11 @@ import { } from '@cedarjs/testing/web/vitest' import { cedarCellTransform } from './plugins/vite-plugin-cedar-cell.js' +import { cedarDevDispatcherPlugin } from './plugins/vite-plugin-cedar-dev-dispatcher.js' import { cedarEntryInjectionPlugin } from './plugins/vite-plugin-cedar-entry-injection.js' import { cedarHtmlEnvPlugin } from './plugins/vite-plugin-cedar-html-env.js' import { cedarNodePolyfills } from './plugins/vite-plugin-cedar-node-polyfills.js' import { cedarRemoveFromBundle } from './plugins/vite-plugin-cedar-remove-from-bundle.js' -import { cedarWaitForApiServer } from './plugins/vite-plugin-cedar-wait-for-api-server.js' import { cedarjsResolveCedarStyleImportsPlugin } from './plugins/vite-plugin-cedarjs-resolve-cedar-style-imports.js' import { cedarTransformJsAsJsx } from './plugins/vite-plugin-jsx-loader.js' import { cedarMergedConfig } from './plugins/vite-plugin-merged-config.js' @@ -32,6 +32,10 @@ export { cedarjsJobPathInjectorPlugin } from './plugins/vite-plugin-cedarjs-job- export { cedarTransformJsAsJsx } from './plugins/vite-plugin-jsx-loader.js' export { cedarMergedConfig } from './plugins/vite-plugin-merged-config.js' export { cedarSwapApolloProvider } from './plugins/vite-plugin-swap-apollo-provider.js' +export { cedarUniversalDeployPlugin } from './plugins/vite-plugin-cedar-universal-deploy.js' +export { cedarDevDispatcherPlugin } from './plugins/vite-plugin-cedar-dev-dispatcher.js' +/** @deprecated The default cedar() plugin array now uses cedarDevDispatcherPlugin. This export is kept for apps that reference cedarWaitForApiServer directly. */ +export { cedarWaitForApiServer } from './plugins/vite-plugin-cedar-wait-for-api-server.js' type PluginOptions = { mode?: string | undefined @@ -69,7 +73,7 @@ export function cedar({ mode }: PluginOptions = {}): PluginOption[] { mode === 'test' && cedarJsRouterImportTransformPlugin(), mode === 'test' && createAuthImportTransformPlugin(), mode === 'test' && autoImportsPlugin(), - cedarWaitForApiServer(), + cedarDevDispatcherPlugin(), cedarNodePolyfills(), cedarHtmlEnvPlugin(), cedarEntryInjectionPlugin(), diff --git a/packages/vite/src/plugins/vite-plugin-cedar-dev-dispatcher.ts b/packages/vite/src/plugins/vite-plugin-cedar-dev-dispatcher.ts new file mode 100644 index 0000000000..40dac9de50 --- /dev/null +++ b/packages/vite/src/plugins/vite-plugin-cedar-dev-dispatcher.ts @@ -0,0 +1,220 @@ +import type { IncomingMessage, ServerResponse } from 'node:http' + +import type { Plugin, ViteDevServer } from 'vite' + +import { getConfig } from '@cedarjs/project-config' + +type Fetchable = { fetch(request: Request): Response | Promise } + +let cachedDispatcher: Fetchable | null = null +// Each invalidation increments this counter. The in-flight build closure +// captures the generation at start and checks it before writing +// cachedDispatcher, so a superseded build never overwrites a newer one. +let dispatcherGeneration = 0 +let buildPromise: Promise | null = null + +async function getDispatcher(): Promise { + if (cachedDispatcher !== null) { + return cachedDispatcher + } + + if (buildPromise !== null) { + return buildPromise + } + + // Capture the current generation so we can detect if we've been + // invalidated by the time the build finishes. + const generationAtStart = dispatcherGeneration + + buildPromise = (async () => { + // Recompile api/src/ -> api/dist/ before loading the dispatcher, so the + // dispatcher always reads fresh build artifacts. We use rebuildApi when a + // build context already exists (incremental rebuild is faster), and fall + // back to a full buildApi on the very first run or after a clean. + try { + const { rebuildApi, buildApi } = + await import('@cedarjs/internal/dist/build/api') + try { + await rebuildApi() + } catch { + // rebuildApi can throw if there is no existing build context yet + // (e.g. first run). Fall back to a full build. + await buildApi() + } + } catch (err) { + console.warn( + '[cedar-dev-dispatcher] API compilation failed; serving with last-known-good dist:', + err, + ) + } + + const { buildCedarDispatcher } = + await import('@cedarjs/api-server/udDispatcher') + const { fetchable } = await buildCedarDispatcher() + + // Only commit if we are still the current generation. If invalidate() was + // called while we were building, a newer build will be (or already is) + // in-flight and we must not overwrite cachedDispatcher with our stale + // result. + if (generationAtStart === dispatcherGeneration) { + cachedDispatcher = fetchable + buildPromise = null + } + + return fetchable + })() + + return buildPromise +} + +function invalidateDispatcher() { + cachedDispatcher = null + buildPromise = null + // Increment so any in-flight build can detect it has been superseded. + dispatcherGeneration++ +} + +function isViteInternalRequest(url: string): boolean { + return ( + url.startsWith('/@') || + url.startsWith('/__vite') || + url.startsWith('/__hmr') || + url.includes('?import') || + url.includes('?t=') || + url.includes('?v=') + ) +} + +function isApiRequest(url: string): boolean { + const cedarConfig = getConfig() + const apiUrl = cedarConfig.web.apiUrl.replace(/\/$/, '') + const apiGqlUrl = cedarConfig.web.apiGraphQLUrl ?? apiUrl + '/graphql' + + return ( + url.startsWith(apiUrl) || + url === apiGqlUrl || + url.startsWith(apiGqlUrl + '/') || + url.startsWith(apiGqlUrl + '?') + ) +} + +async function nodeRequestToFetch(req: IncomingMessage): Promise { + const host = req.headers.host ?? 'localhost' + const url = `http://${host}${req.url ?? '/'}` + + const headers = new Headers() + for (const [key, value] of Object.entries(req.headers)) { + if (value === undefined) { + continue + } + + if (Array.isArray(value)) { + for (const v of value) { + headers.append(key, v) + } + } else { + headers.set(key, value) + } + } + + const method = (req.method ?? 'GET').toUpperCase() + const hasBody = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method) + + let body: Buffer | undefined + + if (hasBody) { + body = await new Promise((resolve, reject) => { + const chunks: Buffer[] = [] + req.on('data', (chunk: Buffer) => chunks.push(chunk)) + req.on('end', () => resolve(Buffer.concat(chunks))) + req.on('error', reject) + }) + } + + return new Request(url, { + method, + headers, + body: hasBody && body && body.length > 0 ? new Uint8Array(body) : undefined, + }) +} + +async function fetchResponseToNode( + fetchRes: Response, + res: ServerResponse, +): Promise { + res.statusCode = fetchRes.status + + fetchRes.headers.forEach((value, key) => { + res.setHeader(key, value) + }) + + const bodyBuffer = await fetchRes.arrayBuffer() + + if (bodyBuffer.byteLength > 0) { + res.end(Buffer.from(bodyBuffer)) + } else { + res.end() + } +} + +export function cedarDevDispatcherPlugin(): Plugin { + return { + name: 'cedar-dev-dispatcher', + apply: 'serve', + + configureServer(server: ViteDevServer) { + server.watcher.on('change', (filePath: string) => { + if (filePath.includes('/api/src/')) { + invalidateDispatcher() + } + }) + + server.middlewares.use( + async (req: IncomingMessage, res: ServerResponse, next: () => void) => { + const url = req.url ?? '/' + + if (isViteInternalRequest(url)) { + return next() + } + + if (!isApiRequest(url)) { + return next() + } + + try { + const dispatcher = await getDispatcher() + const fetchRequest = await nodeRequestToFetch(req) + const fetchResponse = await dispatcher.fetch(fetchRequest) + await fetchResponseToNode(fetchResponse, res) + } catch (err) { + console.error( + '[cedar-dev-dispatcher] Error handling API request:', + err, + ) + + if (!res.headersSent) { + res.writeHead(500, { 'Content-Type': 'application/json' }) + } + + res.end( + JSON.stringify( + { + errors: [ + { + message: + err instanceof Error + ? err.message + : 'Internal Server Error', + }, + ], + }, + null, + 2, + ), + ) + } + }, + ) + }, + } +} diff --git a/packages/vite/src/plugins/vite-plugin-cedar-universal-deploy.ts b/packages/vite/src/plugins/vite-plugin-cedar-universal-deploy.ts new file mode 100644 index 0000000000..36df9bad1e --- /dev/null +++ b/packages/vite/src/plugins/vite-plugin-cedar-universal-deploy.ts @@ -0,0 +1,56 @@ +import { addEntry, catchAllEntry } from '@universal-deploy/store' +import type { Plugin } from 'vite' + +export interface CedarUniversalDeployPluginOptions { + apiRootPath?: string +} + +const VIRTUAL_CEDAR_API = 'virtual:cedar-api' +const RESOLVED_VIRTUAL_CEDAR_API = '\0virtual:cedar-api' + +export function cedarUniversalDeployPlugin( + options: CedarUniversalDeployPluginOptions = {}, +): Plugin { + const { apiRootPath } = options + + return { + name: 'cedar-universal-deploy', + apply: 'build', + + buildStart() { + addEntry({ + id: VIRTUAL_CEDAR_API, + route: '/**', + }) + }, + + resolveId(id) { + if (id === catchAllEntry) { + return RESOLVED_VIRTUAL_CEDAR_API + } + + if (id === VIRTUAL_CEDAR_API) { + return RESOLVED_VIRTUAL_CEDAR_API + } + + return undefined + }, + + load(id) { + if (id !== RESOLVED_VIRTUAL_CEDAR_API) { + return undefined + } + + const apiRootPathArg = + apiRootPath !== undefined + ? `{ apiRootPath: ${JSON.stringify(apiRootPath)} }` + : 'undefined' + + return ` +import { buildCedarDispatcher } from '@cedarjs/api-server/udDispatcher'; +const { fetchable } = await buildCedarDispatcher(${apiRootPathArg}); +export default fetchable; +` + }, + } +} diff --git a/yarn.lock b/yarn.lock index 2813d536fb..04be5d684b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2171,6 +2171,8 @@ __metadata: "@types/dotenv-defaults": "npm:^5.0.0" "@types/split2": "npm:4.2.3" "@types/yargs": "npm:17.0.35" + "@universal-deploy/node": "npm:^0.1.6" + "@universal-deploy/store": "npm:^0.2.1" ansis: "npm:4.2.0" chokidar: "npm:3.6.0" dotenv-defaults: "npm:5.0.2" @@ -2184,7 +2186,9 @@ __metadata: pino-abstract-transport: "npm:1.2.0" pretty-bytes: "npm:5.6.0" pretty-ms: "npm:7.0.1" + rou3: "npm:^0.8.1" split2: "npm:4.2.0" + srvx: "npm:^0.11.9" tsx: "npm:4.21.0" typescript: "npm:5.9.3" vitest: "npm:3.2.4" @@ -3762,6 +3766,7 @@ __metadata: "@types/react": "npm:^18.2.55" "@types/ws": "npm:^8" "@types/yargs-parser": "npm:21.0.3" + "@universal-deploy/node": "npm:^0.1.6" "@vitejs/plugin-react": "npm:4.7.0" "@whatwg-node/fetch": "npm:0.10.13" "@whatwg-node/server": "npm:0.10.18" @@ -3791,6 +3796,11 @@ __metadata: vitest: "npm:3.2.4" ws: "npm:8.20.0" yargs-parser: "npm:21.1.1" + peerDependencies: + "@cedarjs/api-server": "workspace:*" + peerDependenciesMeta: + "@cedarjs/api-server": + optional: false bin: rw-dev-fe: ./dist/devFeServer.js rw-serve-fe: ./dist/runFeServer.js @@ -11520,6 +11530,37 @@ __metadata: languageName: node linkType: hard +"@universal-deploy/node@npm:^0.1.6": + version: 0.1.6 + resolution: "@universal-deploy/node@npm:0.1.6" + dependencies: + "@universal-deploy/store": "npm:^0.2.1" + magic-string: "npm:^0.30.21" + srvx: "npm:^0.11.9" + peerDependencies: + vite: ">=7.1" + peerDependenciesMeta: + vite: + optional: true + checksum: 10c0/2fcabae33a015644c7cb24f2a90e61cf4c10bbd505493bfb1cb5ccf6599974bc9b14343a057ff487748eb55967ebda92632d17412ed8b7fde347adb70de60c34 + languageName: node + linkType: hard + +"@universal-deploy/store@npm:^0.2.1": + version: 0.2.1 + resolution: "@universal-deploy/store@npm:0.2.1" + dependencies: + rou3: "npm:^0.8.1" + srvx: "npm:*" + peerDependencies: + srvx: "*" + peerDependenciesMeta: + srvx: + optional: true + checksum: 10c0/8079a2d41d17b5b9a8d3dc5859ca18875d989fb695d592bee7a8dff13dfdf34af682384c4cc577ed10b2fbba9686953bdcb6ce4fb528548f10c5a8f9191ed8fe + languageName: node + linkType: hard + "@vitejs/plugin-react@npm:4.7.0": version: 4.7.0 resolution: "@vitejs/plugin-react@npm:4.7.0" @@ -26809,6 +26850,13 @@ __metadata: languageName: unknown linkType: soft +"rou3@npm:^0.8.1": + version: 0.8.1 + resolution: "rou3@npm:0.8.1" + checksum: 10c0/c8728cf3c41833db0e20cbadba07b3c678b8b9fb12db1d8803f275a7a6cce02d0be9bee79367575883f65659c9c0ed1001e6527146ed27772e439e5d6c68d264 + languageName: node + linkType: hard + "run-applescript@npm:^7.0.0": version: 7.1.0 resolution: "run-applescript@npm:7.1.0" @@ -27654,6 +27702,15 @@ __metadata: languageName: node linkType: hard +"srvx@npm:*, srvx@npm:^0.11.9": + version: 0.11.15 + resolution: "srvx@npm:0.11.15" + bin: + srvx: bin/srvx.mjs + checksum: 10c0/3f72be7bfb321ad21ae7698a721f1a16b855313d1fa8498a0d68adbec65f8f2d2c5a83cf37849f6489c7403870535a70958976636df3d5274cd785b61b7aa635 + languageName: node + linkType: hard + "ssh2@npm:^1.14.0": version: 1.17.0 resolution: "ssh2@npm:1.17.0"