import { existsSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import * as sys from "@lazarv/react-server/lib/sys.mjs";
import {
banner,
createAdapter,
mergeTomlConfig,
message,
writeJSON,
writeToml,
} from "@lazarv/react-server/adapters/core";
const cwd = sys.cwd();
const cloudflareDir = join(cwd, ".cloudflare");
const outDir = cloudflareDir;
const outStaticDir = join(outDir, "static");
const outServerDir = join(outDir, "worker");
const adapterDir = dirname(fileURLToPath(import.meta.url));
/**
* Build options that the Cloudflare adapter requires.
* These are automatically applied when using this adapter.
*/
export const buildOptions = {
// Enable edge build mode:
// - Adds the edge entry as an input to the build
// - Bundles react-server internals into a shared chunk
// - Route modules import from the shared chunk (no bare specifiers at runtime)
edge: {
// The entry point for the edge worker
entry: join(adapterDir, "worker/edge.mjs"),
},
};
export const adapter = createAdapter({
name: "Cloudflare Worker",
outDir,
outStaticDir,
outServerDir,
handler: async function ({ adapterOptions, options, reactServerOutDir }) {
// Create wrangler.toml configuration
banner("creating Cloudflare Worker configuration", { emoji: "⚙️" });
// Try to get app name from adapter options or package.json
let appName = adapterOptions?.name;
if (!appName) {
const packageJsonPath = join(cwd, "package.json");
if (existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(
readFileSync(packageJsonPath, "utf-8")
);
// Remove scope from package name (e.g., @scope/name -> name)
appName = packageJson.name?.replace(/^@[^/]+\//, "");
} catch {
// Ignore parsing errors
}
}
}
const wranglerConfig = {
name: appName ?? "react-server-app",
main: `.cloudflare/worker/${reactServerOutDir}/server/edge.mjs`,
compatibility_date:
adapterOptions?.compatibilityDate ??
new Date().toISOString().split("T")[0],
compatibility_flags: [
"nodejs_compat",
...(adapterOptions?.compatibilityFlags ?? []),
],
find_additional_modules: true,
base_dir: `.cloudflare/worker/${reactServerOutDir}`,
rules: [
{
type: "ESModule",
globs: ["server/**/*.mjs"],
fallthrough: true,
},
{
type: "Text",
globs: ["**/*.json"],
fallthrough: true,
},
],
assets: {
directory: ".cloudflare/static",
binding: "ASSETS",
...adapterOptions?.assets,
},
...(options.sourcemap ? { upload_source_maps: true } : {}),
...(typeof adapterOptions?.wrangler === "object"
? adapterOptions.wrangler
: {}),
};
// Read existing wrangler.toml if present and merge with adapter config
const existingWranglerPath = join(cwd, "react-server.wrangler.toml");
const finalConfig = mergeTomlConfig(existingWranglerPath, wranglerConfig);
if (existsSync(existingWranglerPath)) {
message(
"merging",
"existing react-server.wrangler.toml with adapter config"
);
}
message("creating", "wrangler.toml");
await writeToml(join(cwd, "wrangler.toml"), finalConfig);
// Create _routes.json for Cloudflare Pages if needed
if (adapterOptions?.pages !== false) {
message("creating", "_routes.json");
await writeJSON(join(outStaticDir, "_routes.json"), {
version: 1,
include: ["/*"],
exclude: [
"/assets/*",
"/client/*",
"/*.ico",
"/*.png",
"/*.jpg",
"/*.jpeg",
"/*.gif",
"/*.svg",
"/*.webp",
"/*.css",
"/*.js",
"/*.woff",
"/*.woff2",
"/*.ttf",
"/*.eot",
...(adapterOptions?.excludeRoutes ?? []),
],
});
}
},
deploy: {
command: "wrangler",
args: ["deploy"],
},
});
export default function defineConfig(adapterOptions) {
return async (_, root, options) => adapter(adapterOptions, root, options);
}