Skip to content

Commit

Permalink
feat(cloudflare): generated wrangler configuration (#2949)
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 authored Feb 3, 2025
1 parent bbcf282 commit e9e920a
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 70 deletions.
13 changes: 0 additions & 13 deletions playground/wrangler.toml

This file was deleted.

35 changes: 26 additions & 9 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 All @@ -17,14 +22,20 @@ const cloudflareExternals = [
"cloudflare:workflows",
] as const;

// TODO: Remove when wrangler -C support landed
// https://github.com/cloudflare/workers-sdk/pull/7994
const isWindows = process.platform === "win32";
const commandWithDir = (command: string) =>
isWindows ? `cmd /c "cd ./ && ${command}"` : `(cd ./ && ${command})`;

const cloudflarePages = defineNitroPreset(
{
extends: "cloudflare",
entry: "./runtime/cloudflare-pages",
exportConditions: ["workerd"],
commands: {
preview: "npx wrangler pages dev ./",
deploy: "npx wrangler pages deploy ./",
preview: commandWithDir("npx wrangler pages dev"),
deploy: commandWithDir("npx wrangler pages deploy"),
},
output: {
dir: "{{ rootDir }}/dist",
Expand Down Expand Up @@ -52,7 +63,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 @@ -71,12 +85,14 @@ const cloudflarePagesStatic = defineNitroPreset(
publicDir: "{{ output.dir }}/{{ baseURL }}",
},
commands: {
preview: "npx wrangler pages dev dist",
deploy: "npx wrangler pages deploy dist",
preview: commandWithDir("npx wrangler pages dev"),
deploy: commandWithDir("npx wrangler pages deploy"),
},
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 +189,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: commandWithDir("npx wrangler dev"),
deploy: commandWithDir("npx wrangler deploy"),
},
unenv: {
external: [...cloudflareExternals],
Expand All @@ -192,6 +208,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
15 changes: 14 additions & 1 deletion src/presets/cloudflare/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import type {
TraceItem,
} from "@cloudflare/workers-types";
import type { DurableObject } from "cloudflare:workers";
import type { Config as WranglerConfig } from "./wrangler/config";

import type {
Config as _Config,
ComputedFields as _ComputedFields,
} from "./wrangler/config";

export type WranglerConfig = Partial<Omit<_Config, keyof _ComputedFields>>;

/**
* https://developers.cloudflare.com/pages/platform/functions/routing/#functions-invocation-routes
Expand All @@ -28,6 +34,13 @@ export interface CloudflareOptions {
*/
wrangler?: WranglerConfig;

/**
* Disable the automatic generation of .wrangler/deploy/config.json
*
* More info: https://developers.cloudflare.com/workers/wrangler/configuration#generated-wrangler-configuration
*/
noWranglerDeployConfig?: boolean;

pages: {
/**
* Nitro will automatically generate a `_routes.json` that controls which files get served statically and
Expand Down
160 changes: 113 additions & 47 deletions src/presets/cloudflare/utils.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,22 @@
import { existsSync, promises as fsp } from "node:fs";
import { parseTOML, stringifyTOML } from "confbox";
import defu from "defu";
import { globby } from "globby";
import type { Nitro } from "nitropack/types";
import type { WranglerConfig, CloudflarePagesRoutes } from "./types";
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 { join, resolve } from "pathe";
import { isCI } from "std-env";
import {
joinURL,
hasProtocol,
withLeadingSlash,
withTrailingSlash,
withoutLeadingSlash,
} from "ufo";
import type { CloudflarePagesRoutes } from "./types";

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 +25,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 +98,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 +120,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 +133,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 +160,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 +173,112 @@ 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;

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 = {
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
);
if (existsSync(configPath)) {
configFromFile = parseTOML<WranglerConfig>(
await fsp.readFile(configPath, "utf8")

// Write wrangler.json
await writeFile(
wranglerConfigPath,
JSON.stringify(wranglerConfig, null, 2),
true
);

// 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;
}

0 comments on commit e9e920a

Please sign in to comment.