Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cloudflare): generated wrangler configuration #2949

Merged
merged 16 commits into from
Feb 3, 2025
14 changes: 6 additions & 8 deletions playground/wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@ name = "nitro-test"

compatibility_date = "2024-09-19"

assets = { directory = "./.output/public/", binding = "ASSETS" }
# [[durable_objects.bindings]]
# name = "$DurableObject"
# class_name = "$DurableObject"

[[durable_objects.bindings]]
name = "$DurableObject"
class_name = "$DurableObject"

[[migrations]]
tag = "v1"
new_sqlite_classes = ["$DurableObject"]
# [[migrations]]
# tag = "v1"
# new_sqlite_classes = ["$DurableObject"]
21 changes: 16 additions & 5 deletions src/presets/cloudflare/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { defineNitroPreset } from "nitropack/kit";
import { writeFile } from "nitropack/kit";
import type { Nitro } from "nitropack/types";
import { resolve } from "pathe";
import { writeCFPagesFiles, writeCFPagesStaticFiles } from "./utils";
import {
writeWranglerConfig,
writeCFRoutes,
writeCFPagesHeaders,
writeCFPagesRedirects,
} from "./utils";

export type { CloudflareOptions as PresetOptions } from "./types";

Expand Down Expand Up @@ -52,7 +57,10 @@ const cloudflarePages = defineNitroPreset(
},
hooks: {
async compiled(nitro: Nitro) {
await writeCFPagesFiles(nitro);
await writeWranglerConfig(nitro, true /* pages */);
await writeCFRoutes(nitro);
await writeCFPagesHeaders(nitro);
await writeCFPagesRedirects(nitro);
},
},
},
Expand All @@ -76,7 +84,9 @@ const cloudflarePagesStatic = defineNitroPreset(
},
hooks: {
async compiled(nitro: Nitro) {
await writeCFPagesStaticFiles(nitro);
await writeWranglerConfig(nitro, true /* pages */);
await writeCFPagesHeaders(nitro);
await writeCFPagesRedirects(nitro);
},
},
},
Expand Down Expand Up @@ -173,8 +183,8 @@ const cloudflareModule = defineNitroPreset(
entry: "./runtime/cloudflare-module",
exportConditions: ["workerd"],
commands: {
preview: "npx wrangler dev ./server/index.mjs --assets ./public/",
deploy: "npx wrangler deploy",
preview: "npx wrangler dev -c ./server/wrangler.json",
pi0 marked this conversation as resolved.
Show resolved Hide resolved
deploy: "npx wrangler deploy -c ./server/wrangler.json",
},
unenv: {
external: [...cloudflareExternals],
Expand All @@ -192,6 +202,7 @@ const cloudflareModule = defineNitroPreset(
},
hooks: {
async compiled(nitro: Nitro) {
await writeWranglerConfig(nitro, false /* module */);
await writeFile(
resolve(nitro.options.output.dir, "package.json"),
JSON.stringify({ private: true, main: "./server/index.mjs" }, null, 2)
Expand Down
5 changes: 5 additions & 0 deletions src/presets/cloudflare/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export interface CloudflareOptions {
*/
wrangler?: WranglerConfig;

/**
* Disable the automatic generation of .wrangler/deploy/config.json
*/
noWranglerDeployConfig?: boolean;

pages: {
/**
* Nitro will automatically generate a `_routes.json` that controls which files get served statically and
Expand Down
4 changes: 3 additions & 1 deletion src/presets/cloudflare/types.wrangler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
* - `@breaking`: the deprecation/optionality is a breaking change from Wrangler v1.
* - `@todo`: there's more work to be done (with details attached).
*/
export type Config = ConfigFields<DevConfig> & PagesConfigFields & Environment;
export type Config = Partial<
ConfigFields<DevConfig> & PagesConfigFields & Environment
>;

export type RawConfig = Partial<ConfigFields<RawDevConfig>> &
PagesConfigFields &
Expand Down
161 changes: 116 additions & 45 deletions src/presets/cloudflare/utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { existsSync, promises as fsp } from "node:fs";
import { parseTOML, stringifyTOML } from "confbox";
import defu from "defu";
import { existsSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { relative, dirname } from "node:path";
import { writeFile } from "nitropack/kit";
import { parseTOML } from "confbox";
import { defu } from "defu";
import { globby } from "globby";
import type { Nitro } from "nitropack/types";
import { join, resolve } from "pathe";
import { isCI } from "std-env";
import {
joinURL,
hasProtocol,
Expand All @@ -13,20 +15,9 @@ import {
withoutLeadingSlash,
} from "ufo";
import type { CloudflarePagesRoutes } from "./types";
import type { Config as WranglerConfig } from "./types.wrangler";

export async function writeCFPagesFiles(nitro: Nitro) {
await writeCFRoutes(nitro);
await writeCFPagesHeaders(nitro);
await writeCFPagesRedirects(nitro);
await writeCFWrangler(nitro);
}

export async function writeCFPagesStaticFiles(nitro: Nitro) {
await writeCFPagesHeaders(nitro);
await writeCFPagesRedirects(nitro);
}

async function writeCFRoutes(nitro: Nitro) {
export async function writeCFRoutes(nitro: Nitro) {
const _cfPagesConfig = nitro.options.cloudflare?.pages || {};
const routes: CloudflarePagesRoutes = {
version: _cfPagesConfig.routes?.version || 1,
Expand All @@ -35,9 +26,10 @@ async function writeCFRoutes(nitro: Nitro) {
};

const writeRoutes = () =>
fsp.writeFile(
writeFile(
resolve(nitro.options.output.dir, "_routes.json"),
JSON.stringify(routes, undefined, 2)
JSON.stringify(routes, undefined, 2),
true
);

if (_cfPagesConfig.defaultRoutes === false) {
Expand Down Expand Up @@ -107,7 +99,7 @@ function comparePaths(a: string, b: string) {
return a.split("/").length - b.split("/").length || a.localeCompare(b);
}

async function writeCFPagesHeaders(nitro: Nitro) {
export async function writeCFPagesHeaders(nitro: Nitro) {
const headersPath = join(nitro.options.output.dir, "_headers");
const contents = [];

Expand All @@ -129,7 +121,7 @@ async function writeCFPagesHeaders(nitro: Nitro) {
}

if (existsSync(headersPath)) {
const currentHeaders = await fsp.readFile(headersPath, "utf8");
const currentHeaders = await readFile(headersPath, "utf8");
if (/^\/\* /m.test(currentHeaders)) {
nitro.logger.info(
"Not adding Nitro fallback to `_headers` (as an existing fallback was found)."
Expand All @@ -142,10 +134,10 @@ async function writeCFPagesHeaders(nitro: Nitro) {
contents.unshift(currentHeaders);
}

await fsp.writeFile(headersPath, contents.join("\n"));
await writeFile(headersPath, contents.join("\n"), true);
}

async function writeCFPagesRedirects(nitro: Nitro) {
export async function writeCFPagesRedirects(nitro: Nitro) {
const redirectsPath = join(nitro.options.output.dir, "_redirects");
const staticFallback = existsSync(
join(nitro.options.output.publicDir, "404.html")
Expand All @@ -169,7 +161,7 @@ async function writeCFPagesRedirects(nitro: Nitro) {
}

if (existsSync(redirectsPath)) {
const currentRedirects = await fsp.readFile(redirectsPath, "utf8");
const currentRedirects = await readFile(redirectsPath, "utf8");
if (/^\/\* /m.test(currentRedirects)) {
nitro.logger.info(
"Not adding Nitro fallback to `_redirects` (as an existing fallback was found)."
Expand All @@ -182,37 +174,116 @@ async function writeCFPagesRedirects(nitro: Nitro) {
contents.unshift(currentRedirects);
}

await fsp.writeFile(redirectsPath, contents.join("\n"));
await writeFile(redirectsPath, contents.join("\n"), true);
}

async function writeCFWrangler(nitro: Nitro) {
type WranglerConfig = typeof nitro.options.cloudflare.wrangler;
// https://developers.cloudflare.com/workers/wrangler/configuration/#generated-wrangler-configuration
export async function writeWranglerConfig(nitro: Nitro, isPages: boolean) {
// Compute path to generated wrangler.json
const wranglerConfigDir = nitro.options.output.serverDir;
const wranglerConfigPath = join(wranglerConfigDir, "wrangler.json");

const inlineConfig: WranglerConfig =
nitro.options.cloudflare?.wrangler || ({} as WranglerConfig);
// Default configs
const defaults: WranglerConfig = {};

// Write wrangler.toml only if config is not empty
if (!inlineConfig || Object.keys(inlineConfig).length === 0) {
return;
// Compatibility date
defaults.compatibility_date =
nitro.options.compatibilityDate.cloudflare ||
nitro.options.compatibilityDate.default;

// Enable nodejs compatibility by default but disable wrangler transforms
defaults.compatibility_flags = ["nodejs_compat", "no_nodejs_compat_v2"];

if (isPages) {
// Pages
defaults.pages_build_output_dir = relative(
dirname(wranglerConfigPath),
nitro.options.output.publicDir
);
} else {
// Modules
defaults.main = relative(
wranglerConfigDir,
join(nitro.options.output.serverDir, "index.mjs")
);
defaults.assets = {
// @ts-expect-error
binding: "ASSETS",
directory: relative(
dirname(wranglerConfigPath),
nitro.options.output.publicDir
),
};
}

let configFromFile: WranglerConfig = {} as WranglerConfig;
const configPath = resolve(
nitro.options.rootDir,
inlineConfig.configPath || "wrangler.toml"
// Read user config
const userConfig = await resolveWranglerConfig(nitro.options.rootDir);

// (first argument takes precedence)
const wranglerConfig = mergeWranglerConfigs(
userConfig,
nitro.options.cloudflare?.wrangler,
defaults
);

// Write wrangler.json
await writeFile(
wranglerConfigPath,
JSON.stringify(wranglerConfig, null, 2),
true
);
if (existsSync(configPath)) {
configFromFile = parseTOML<WranglerConfig>(
await fsp.readFile(configPath, "utf8")

// Write .wrangler/deploy/config.json (redirect file)
if (!nitro.options.cloudflare?.noWranglerDeployConfig) {
const configPath = join(
nitro.options.rootDir,
".wrangler/deploy/config.json"
);
await writeFile(
configPath,
JSON.stringify({
configPath: relative(dirname(configPath), wranglerConfigPath),
}),
true
);
}
}

async function resolveWranglerConfig(dir: string): Promise<WranglerConfig> {
const jsonConfig = join(dir, "wrangler.json");
if (existsSync(jsonConfig)) {
const config = JSON.parse(
await readFile(join(dir, "wrangler.json"), "utf8")
) as WranglerConfig;
return config;
}
const tomlConfig = join(dir, "wrangler.toml");
if (existsSync(tomlConfig)) {
const config = parseTOML<WranglerConfig>(
await readFile(join(dir, "wrangler.toml"), "utf8")
);
return config;
}
return {};
}

const wranglerConfig: WranglerConfig = defu(configFromFile, inlineConfig);
/**
* Merge wrangler configs (first argument takes precedence)
*/
function mergeWranglerConfigs(
...configs: (WranglerConfig | undefined)[]
): WranglerConfig {
// Merge configs
const merged = defu({}, ...configs) as WranglerConfig;

const wranglerPath = join(
isCI ? nitro.options.rootDir : nitro.options.buildDir,
"wrangler.toml"
);
// Normalize compatibility flags
if (merged.compatibility_flags) {
let flags = [...new Set(merged.compatibility_flags || [])];
if (flags.includes("no_nodejs_compat_v2")) {
flags = flags.filter((flag) => flag !== "nodejs_compat_v2");
}
merged.compatibility_flags = flags;
}

await fsp.writeFile(wranglerPath, stringifyTOML(wranglerConfig));
return merged;
}