Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 31 additions & 9 deletions bun.lock

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,16 @@
"@types/bun": "latest",
"@types/node": "latest",
"@typescript/native-preview": "7.0.0-dev.20260611.2",
"@vitejs/plugin-react": "^6.0.2",
"@vitejs/plugin-rsc": "^0.5.27",
"ai": "^6.0.62",
"aws4fetch": "^1.0.20",
"better-auth": "^1.6.2",
"drizzle-kit": ">=1.0.0-rc.1",
"drizzle-orm": ">=1.0.0-rc.1",
"effect": ">=4.0.0-beta.78 || >=4.0.0",
"fast-xml-parser": "^5.3.4",
"react-router": "7.16.0",
"rolldown": "1.0.1",
"solid-js": "latest",
"sonda": "^0.11.1",
Expand Down Expand Up @@ -151,4 +154,4 @@
"typescript": "latest",
"yaml": "^2.8.2"
}
}
}
5 changes: 5 additions & 0 deletions packages/alchemy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -340,9 +340,14 @@
"@types/pg": "^8.11.0",
"@types/picomatch": "^4.0.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.3",
"@types/ws": "^8.18.1",
"@vitejs/plugin-react": "catalog:",
"@vitejs/plugin-rsc": "catalog:",
"better-auth": "catalog:",
"effect": "catalog:",
"react-dom": "^19.2.7",
"react-router": "catalog:",
"react-devtools-core": "^7.0.1",
"solid-js": "catalog:",
"tsconfig-paths": "^4.2.0",
Expand Down
30 changes: 30 additions & 0 deletions packages/alchemy/src/Bundle/Bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as Queue from "effect/Queue";
import * as Schema from "effect/Schema";
import * as Stream from "effect/Stream";
import assert from "node:assert";
import nodePath from "node:path";
import * as rolldown from "rolldown";
import { sha256, sha256Object } from "../Util/sha256.ts";
import {
Expand Down Expand Up @@ -57,6 +58,7 @@ export interface BundleFile {
readonly path: string;
readonly content: string | Uint8Array<ArrayBufferLike>;
readonly hash: string;
readonly contentType?: string;
}

export class BundleError extends Schema.TaggedErrorClass<BundleError>()(
Expand Down Expand Up @@ -319,12 +321,38 @@ export function bundleOutputFromFiles(
files.map((file) => ({
path: file.path,
hash: file.hash,
contentType: file.contentType,
})),
),
(hash) => ({ files, hash }),
);
}

export const contentTypeFromPath = (filePath: string) => {
switch (nodePath.extname(filePath)) {
case ".wasm":
return "application/wasm";
case ".txt":
case ".html":
case ".sql":
case ".custom":
return "text/plain";
case ".bin":
return "application/octet-stream";
case ".json":
return "application/json";
case ".mjs":
case ".js":
return "application/javascript+module";
case ".cjs":
return "application/javascript";
case ".map":
return "application/source-map";
default:
return "application/octet-stream";
}
};

function bundleFileFromOutputChunk(
chunk: rolldown.OutputChunk | rolldown.OutputAsset,
): Effect.Effect<BundleFile> {
Expand All @@ -334,12 +362,14 @@ function bundleFileFromOutputChunk(
path: chunk.fileName,
content: chunk.code,
hash,
contentType: contentTypeFromPath(chunk.fileName),
}));
case "asset":
return Effect.map(sha256(chunk.source), (hash) => ({
path: chunk.fileName,
content: chunk.source,
hash,
contentType: contentTypeFromPath(chunk.fileName),
}));
}
}
120 changes: 107 additions & 13 deletions packages/alchemy/src/Cloudflare/Website/Vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type WorkerBindingProps,
type WorkerProps,
} from "../Workers/Worker.ts";
import type { CloudflareVitePluginOptionsWithAssets } from "../Workers/Vite.ts";
export interface ViteProps<
Bindings extends WorkerBindingProps = {},
> extends Omit<WorkerProps<Bindings>, "vite" | "main" | "assets"> {
Expand All @@ -32,6 +33,11 @@ export interface ViteProps<
* Supports `runWorkerFirst`, `htmlHandling`, `notFoundHandling`, etc.
*/
assets?: AssetsConfig;
/**
* Advanced Vite environment topology for Worker builds. RSC apps usually
* run the Worker in the `rsc` environment and load `ssr` as a child.
*/
viteEnvironment?: CloudflareVitePluginOptionsWithAssets["viteEnvironment"];
}

/**
Expand All @@ -49,6 +55,26 @@ export interface ViteProps<
* @product Website
* @category Workers & Compute
*
* @section Vite Config vs Cloudflare.Vite
* Keep framework configuration in `vite.config.ts`: React, Vue, Tailwind,
* React Router/RSC plugins, framework entries, and extra Vite build inputs
* belong there.
*
* Keep Cloudflare and Alchemy configuration in `Cloudflare.Vite`: resource
* bindings, compatibility flags, asset routing, and Worker environment
* topology belong here.
*
* Do not add `@distilled.cloud/cloudflare-vite-plugin` manually to your Vite
* config when using `Cloudflare.Vite`. Alchemy loads the app's normal Vite
* config and injects the distilled Cloudflare Vite plugin programmatically so
* its options stay aligned with Alchemy's resources, bindings, asset settings,
* compatibility settings, deploy diffs, and local dev runtime.
*
* Plain `vite dev` can still be useful for framework-only work, but it does
* not provide Alchemy-managed Cloudflare bindings. Use `alchemy dev` for the
* authoritative local Worker dev path when the app depends on Alchemy
* resources.
*
* @section Deploying a Static Site
* For a pure static site (no SSR), a single call is all you need.
* Vite builds the project and Alchemy deploys the output as a
Expand Down Expand Up @@ -79,7 +105,28 @@ export interface ViteProps<
* flags: ["nodejs_compat"],
* },
* assets: {
* config: { runWorkerFirst: true },
* runWorkerFirst: true,
* },
* });
* ```
*
* @section React Server Components
* For RSC frameworks that use Vite child environments, pass the Worker
* topology through `viteEnvironment`. Alchemy requires the distilled build
* manifest for this topology so it can upload the full Worker module set. The
* framework's RSC entries still belong in `vite.config.ts`; `viteEnvironment`
* tells Alchemy which Vite environment is the Cloudflare Worker and which child
* environments must be available to it at runtime.
*
* @example RSC topology
* ```typescript
* const app = yield* Cloudflare.Vite("ReactRouter", {
* compatibility: {
* flags: ["nodejs_compat"],
* },
* viteEnvironment: {
* name: "rsc",
* childEnvironments: ["ssr"],
* },
* });
* ```
Expand All @@ -95,12 +142,55 @@ export interface ViteProps<
* flags: ["nodejs_compat"],
* },
* assets: {
* config: {
* htmlHandling: "auto-trailing-slash",
* notFoundHandling: "single-page-application",
* },
* htmlHandling: "auto-trailing-slash",
* notFoundHandling: "single-page-application",
* },
* });
* ```
*
* @section Vite Worker With Durable Objects
* For Vite apps that own their Worker entrypoint, configure the entry in the
* Vite project (usually via the framework plugin, or `environments.ssr.build`
* for custom apps). Export the default Worker handler and any local Durable
* Object classes from that Vite entry. Alchemy deploys the Vite-built Worker
* module set and attaches the bindings, Durable Object metadata, migrations,
* compatibility settings, and assets to the same Worker script.
* Declare each local Durable Object with `Cloudflare.DurableObjectNamespace` in
* `env`; exporting the class from the Vite entry makes it available to the
* Worker module, while the `env` binding is what gives Alchemy ownership of the
* namespace and migrations.
*
* @example One Worker With A Local Durable Object
* ```typescript
* // alchemy.run.ts
* import type { Counter } from "./src/worker.ts";
*
* const app = yield* Cloudflare.Vite("App", {
* env: {
* Counter: Cloudflare.DurableObjectNamespace<Counter>("Counter", {
* className: "Counter",
* }),
* },
* assets: {
* runWorkerFirst: ["/api/*"],
* },
* });
*
* // src/worker.ts
* import { DurableObject } from "cloudflare:workers";
*
* export class Counter extends DurableObject {
* async increment() {
* return 1;
* }
* }
*
* export default {
* async fetch(request, env) {
* const count = await env.Counter.getByName("main").increment();
* return Response.json({ count });
* },
* };
* ```
*
* @section Custom Rebuild Scope
Expand Down Expand Up @@ -170,13 +260,17 @@ export const Vite: {
id,
Effect.map(
Effect.isEffect(propsEff) ? propsEff : Effect.succeed(propsEff),
(props) => ({
...props,
main: undefined!,
vite: {
rootDir: props?.rootDir,
memo: props?.memo,
},
}),
(props) => {
const viteEnvironment = props?.viteEnvironment;
return {
...props,
main: undefined!,
vite: {
rootDir: props?.rootDir,
memo: props?.memo,
viteEnvironment,
},
};
},
),
)) as any;
Original file line number Diff line number Diff line change
Expand Up @@ -383,19 +383,20 @@ export const LocalWorkerProvider = () =>

const runVite = Effect.fnUntraced(function* (
worker: WorkerConfig,
rootDir: string | undefined,
vite: NonNullable<WorkerProps["vite"]>,
) {
const proxy = yield* maybeStartProxy(worker.id, worker.dev);
yield* proxy.unset().pipe(Effect.forkChild);
// Loaded lazily: `./Vite.ts` pulls in `@distilled.cloud/cloudflare-vite-plugin`
// (~0.5s); only needed when running a vite dev server.
const Vite = yield* Effect.promise(() => import("./Vite.ts"));
const devServer = yield* Vite.viteDev(
rootDir,
vite.rootDir,
worker.env ?? {},
{
compatibilityDate: worker.compatibility.date,
compatibilityFlags: worker.compatibility.flags,
viteEnvironment: vite.viteEnvironment,
worker: {
name: worker.name,
bindings: worker.workerBindings,
Expand Down Expand Up @@ -439,7 +440,7 @@ export const LocalWorkerProvider = () =>
const { props, bindings } = options;
const config = yield* buildConfig(options);
const url = yield* (
props.vite ? runVite(config, props.vite.rootDir) : runWorker(config)
props.vite ? runVite(config, props.vite) : runWorker(config)
).pipe(Effect.map((url) => url.toString()));
return {
workerId: config.name,
Expand Down
Loading
Loading