1# @alchemy.run/frontend-frameworks/core
2
3Platform-neutral core for framework integrations (Vite, Waku, Astro, SvelteKit, Next.js):
4
5- **`BuildOutput` contract** — `{ clientDirectory, serverModules (entry first, sha256-hashed), externalWorkspaces }`, plus standalone persistence helpers (`writeBuildOutput` / `readBuildOutput`) used by the e2e harness for its `dist/build.json` (persistence is a harness/testing concern, not part of the `Framework` contract).
6- **Build-output collector** — the `alchemy:build-output` Vite plugin (`makeBuildOutputCollector`): captures client/server outputs across environments, with `skipEnvironments` (e.g. Astro's `prerender`), deterministic server-entry selection (pins the wrapped `\0distilled:worker-entry:` main), and a post-`buildApp` disk re-read mode (`collect({ fromDisk: true })`) for frameworks that write or prune server modules after the bundler finishes (e.g. Waku).
7- **`readServerModulesFromDisk`** — for frameworks whose final server bundle lives on disk (SvelteKit's rolldown pass, Next.js's `.open-next` output).
8- **`loadProjectModule`** — load the _project's_ `vite`/framework install instead of ours.
9- **`Framework` service contract** — the common effectful `{ build, dev }` service each framework package implements; `build` returns the `BuildOutput` purely in-memory.
10- **`DeployTarget` contract** — the deploy target as a _value_ passed to framework integrations, with Cloudflare Workers as the first implementation. See the architecture section below.
11
12## Architecture: frameworks × deploy targets
13
14A framework integration answers "how do I drive this framework's build and dev
15servers programmatically?". A **deploy target** answers "what platform is the
16server code being produced _for_?". These are orthogonal, and the package
17layout reflects it:
18
19- **framework-core** (this package) is platform-neutral: `BuildOutput`, the
20 collector, the loaders, the `Framework` service, and the generic
21 `DeployTarget` seams below. It never imports a platform SDK or bundler
22 plugin for a specific runtime.
23- **Framework packages** (`@alchemy.run/frontend-frameworks/waku`, `.../astro`,
24 `.../sveltekit`, and later `.../nextjs`) own the framework mechanics —
25 programmatic build/dev entry points, config synthesis, adapter plumbing —
26 and take the deploy target as a **prop** (`target:`) rather than hardcoding
27 Cloudflare.
28- **Per-framework target modules** own the halves that are neither generic
29 nor platform-neutral: waku's cloudflare adapter fork, astro's integration
30 fork, kit's in-memory adapter + post-adapt re-bundle. Each ships as a
31 subpath of its framework package (e.g. `@alchemy.run/frontend-frameworks/waku/cloudflare`
32 exporting the `WakuCloudflareTarget`). A future AWS target is a new subpath
33 (`@alchemy.run/frontend-frameworks/waku/aws`) implementing the same seams — no change to
34 the framework package or to framework-core.
35
36### The `DeployTarget` interface
37
38```ts
39interface DeployTarget<Config = unknown> {
40 /** Stable platform identifier: "cloudflare", "aws", ... */
41 readonly platform: string;
42 /** Opaque target configuration — never inspected by framework-core.
43 * Cloudflare: worker name/bindings/compatibility; AWS later: its own shape. */
44 readonly config: Config;
45 /** The USER's own server entry, when the target config carries one: a module
46 * that wraps the framework's emitted entry (Cloudflare: a worker entry
47 * re-exporting Durable Object classes). `{ main: string }`, relative to the
48 * project root or absolute — resolve via `resolveDeployTargetEntry`. */
49 readonly entry?: DeployTargetEntry;
50 /** Resolve/bundle settings for server code: conditions (e.g. ["workerd",
51 * "worker", "module", "browser"]), externals (e.g. ["cloudflare:"]), mainFields. */
52 readonly bundle?: DeployTargetBundleOptions;
53 /** WHOLESALE build takeover: when defined, the framework package delegates
54 * its entire production build here (the OpenNext case for Next.js).
55 * Most targets leave this undefined and let the framework drive. */
56 readonly build?: (
57 context: DeployTargetBuildContext,
58 ) => Effect<BuildOutput, DeployTargetError, DeployTargetServices>;
59 /** Finishing pass over the framework-produced BuildOutput — the
60 * SvelteKit-style post-adapt re-bundle for the target runtime.
61 * `context.entry` carries the on-disk server entry when the framework's
62 * adapt step wrote one. Run via `applyDeployTargetFinish`. */
63 readonly finish?: (
64 output: BuildOutput,
65 context: DeployTargetFinishContext,
66 ) => Effect<BuildOutput, DeployTargetError, DeployTargetServices>;
67 /** The target's local serving story for BUILT server code (preview-parity:
68 * miniflare / cloudflare-runtime today; a Lambda emulator later).
69 * Scoped — closing the Scope stops the server. Returns { url, fetch? }. */
70 readonly serve?: (
71 context: DeployTargetServeContext,
72 ) => Effect<DeployTargetServer, DeployTargetError, Scope | DeployTargetServices>;
73}
74```
75
76(`DeployTargetServices = FileSystem | Path` — the same services the harness
77provides to `Framework` layers.)
78
79Only these seams are generic. Everything else a platform needs — the vite
80plugin to inject, the adapter to select, dev-server integration that reaches
81inside a framework's toolchain — belongs to the per-framework target module,
82which **extends** `DeployTarget` with framework-specific hooks:
83
84```ts
85// packages/frontend-frameworks/src/waku/cloudflare.ts (illustrative)
86export interface WakuTarget extends DeployTarget<WakuCloudflareConfig> {
87 /** Absolute path of the adapter module waku's config should select. */
88 readonly adapterPath: Effect<string, DeployTargetError>;
89 /** Vite plugins to inject into waku's `vite.plugins` (dev + build). */
90 readonly vitePlugins: (context: ...) => Effect<PluginOption[], ...>;
91}
92export default (config: WakuCloudflareConfig): WakuTarget => ...;
93```
94
95Helpers exported by framework-core:
96
97- `makeDeployTarget(target)` — identity with contract checking; preserves
98 framework-specific extensions.
99- `isDeployTarget(value)` — structural guard (`platform: string` + `config`).
100- `resolveDeployTarget(root, input, config)` — resolves a
101 `DeployTargetInput`: a target value (as-is), a factory
102 (`(config) => target`, applied to `config`), or a module specifier string
103 (loaded from the _project's_ `node_modules` via `loadProjectModule`; the
104 module's `default` — or named `target` — export is the value or factory).
105- `applyDeployTargetFinish(target, output, context)` — run the finishing pass
106 if defined, else pass the build through.
107- `resolveDeployTargetEntry(target, { root })` — the user-entry seam's
108 accessor: the absolute path of `target.entry.main` (resolved against the
109 project root), or `undefined` when no user entry is configured.
110- `selectEntryByFacade(entryPath)` (collector) — a `selectEntry` predicate
111 pinning the chunk whose facade module is that entry file (tolerates the
112 `\0distilled:worker-entry:` wrapper id).
113
114### The user-entry seam (`target.entry`)
115
116Some apps need their **own module** to be the deployed entry instead of the
117framework's — on Cloudflare, a worker entry that wraps the framework's fetch
118handler and additionally exports Durable Object / Workflow classes (they must
119live on the same worker for their bindings to resolve). The seam is designed
120once, cross-framework:
121
1221. **Carriage.** The target's config carries the raw value in its own shape
123 (Cloudflare: the vite plugin's `main` option — so the built-in Vite path
124 honors it natively). The target surfaces it platform-neutrally as
125 `entry: { main }`; framework packages read it via
126 `resolveDeployTargetEntry(target, { root })`.
1272. **Bundler entry.** The framework package makes the user entry the entry
128 module its build/dev pipeline serves, instead of unconditionally pinning
129 the framework's own emitted entry (waku:
130 `makeWakuPluginOptions` honors the user main; dev's module runner serves
131 the wrapped entry, so DO classes exist in dev too).
1323. **Wrappable handler.** The framework package exposes a stable importable
133 specifier for its emitted server handler so the user entry has something
134 to wrap. Convention: `virtual:{framework}/server-entry` (precedent: React
135 Router's `virtual:react-router/server-build`; waku ships
136 `virtual:waku/server-entry`).
1374. **Entry selection.** The chunk built from the user entry becomes
138 `serverModules[0]` (`selectEntryByFacade`); the framework's own entry
139 remains an ordinary chunk the user entry imports.
140
141### How a framework package consumes the target
142
1431. Its options gain `target?: <Framework>TargetInput` (a
144 `DeployTargetInput<<Framework>Target, Config>`), defaulting to its own
145 cloudflare subpath specifier (e.g. `"@alchemy.run/frontend-frameworks/waku/cloudflare"`).
146 Resolve it once per operation with
147 `resolveDeployTarget(root, input, config)` where `config` is the
148 target-scoped configuration the caller passed.
1492. `Framework.build`:
150 - if `target.build` is defined, delegate wholesale (OpenNext).
151 - otherwise run the framework build, consulting the target's
152 framework-specific hooks (plugin/adapter injection) and `target.bundle`
153 where the framework package does its own resolving/bundling, then finish
154 with `applyDeployTargetFinish(target, output, { root, framework, entry })`.
1553. `Framework.dev`: the HMR dev server is framework-owned; target-specific
156 dev integration (the workerd module-runner vite plugin, dev `platform`
157 stubs) comes from the per-framework target hooks. `target.serve` is NOT
158 the HMR path — it serves _built_ output (preview parity).
1594. Never import a platform package (`@alchemy.run/cloudflare-runtime/vite`,
160 `cloudflare-runtime`, a future AWS SDK) from the framework package's core
161 modules — only from its target subpath module.
162
163### Harness carriage (`e2e.config.ts`)
164
165The e2e harness carries target configuration target-scoped:
166
167```ts
168export default Options.make({
169 target: {
170 // name: "cloudflare" is the default (the only implemented target today)
171 cloudflare: {
172 worker: {
173 /* CloudflareVitePluginOptions: compat date/flags, name, bindings, assets */
174 },
175 preview: {
176 /* miniflare options for `e2e preview` / Playwright "live" */
177 },
178 },
179 },
180 framework: "@alchemy.run/frontend-frameworks/waku",
181});
182```
183
184The pre-target top-level fields remain as deprecated aliases
185(`vite` ≙ `target.cloudflare.worker`, `miniflare` ≙
186`target.cloudflare.preview`); `Options.resolveCloudflareOptions(options)` is
187the single accessor that merges them (target-scoped wins). Framework packages
188that read harness options structurally must read
189`options.target?.cloudflare?.worker ?? options.vite`. The harness's miniflare
190preview is exposed as the cloudflare target's `serve`
191(`packages/cloudflare-test-tools/src/e2e/CloudflareTarget.ts`) — serving built output is the
192target's concern; the implementation lives in the harness while miniflare is
193a harness dependency.
194
195### Migration recipe per framework
196
197Common steps: add `target?:` to the framework options (default: the package's
198`./cloudflare` subpath), create `src/<framework>/cloudflare.ts` + a `./cloudflare` export
199map entry, move every cloudflare import out of the core module into it, and
200read harness options via the target-scoped carriage above.
201
202- **Waku** — `WakuTarget` extends `DeployTarget` with the adapter-path hook
203 (today: the wrangler-free fork at `@alchemy.run/frontend-frameworks/waku/adapter`, selected
204 via `unstable_adapter`) and the vite-plugin hook (today:
205 `cloudflareVitePlugin(makeWakuPluginOptions(...))` with the pinned
206 rsc/ssr topology). `Waku.ts` keeps: config synthesis, the SSG
207 preview-server global, the collector, dev/build orchestration.
208- **Astro** — `AstroTarget` supplies the integration (today: the
209 `@astrojs/cloudflare` fork over our vite plugin, with its server
210 entrypoints) injected into `AstroInlineConfig`; `Astro.ts` keeps the
211 programmatic `dev()`/`build()` driving and the collector
212 (`entryEnvironment: "ssr"`, `skipEnvironments: ["prerender"]`).
213- **SvelteKit** — `SvelteKitTarget` supplies the kit `Adapter` instance
214 (today: the in-memory cloudflare adapter fork) and implements the generic
215 `finish` seam (today: the rolldown re-bundle of `_worker.js` for workerd,
216 using `target.bundle` conditions); `SvelteKit.ts` keeps kit config
217 synthesis, `buildApp` driving, and the Node-SSR dev server (the dev
218 `platform` emulation is a target hook).
219- **Next.js** (separate branch; adopts this interface) — the OpenNext
220 pipeline is the canonical `target.build` wholesale takeover: the
221 cloudflare target owns the OpenNext orchestration, the final bundle pass,
222 and dev v1 serving via cloudflare-runtime; the framework package is a thin
223 shell that resolves the target and delegates.
224
225### Doctrine (applies to every target implementation)
226
227- **Wrangler-free, programmatic-only.** No `wrangler.json` is read or
228 written anywhere; every piece of configuration a wrangler file would carry
229 is expressed as in-memory options on the target's `config`. We never spawn
230 a framework's CLI binary (upstream pipelines may internally — e.g. OpenNext
231 spawning `next build` — but that is upstream orchestration, not ours).
232- **Platform-proxy policy.** Wherever an upstream integration reaches for
233 wrangler's `getPlatformProxy` (SvelteKit `adapter-cloudflare`, OpenNext
234 `initOpenNextCloudflareForDev`, Astro `platformProxy`), the replacement is
235 `@alchemy.run/cloudflare-runtime/core/platform-proxy` (workerd-backed
236 Node-side proxies for `env`/`cf`/`ctx`/`caches`, configured in-memory) —
237 never a wrangler dependency. SvelteKit's `makeDevPlatformEmulator` is the
238 first consumer.
239- **Version pinning.** Upstream surfaces the integrations touch are
240 `@experimental`/`unstable_`/unexported: pin exact framework versions,
241 e2e-test against real apps in CI, and treat version bumps as deliberate
242 migrations.