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): experimental config redirection support #2949

Draft
wants to merge 5 commits into
base: v2
Choose a base branch
from
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
5 changes: 5 additions & 0 deletions playground/nitro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,9 @@ import { defineNitroConfig } from "nitropack/config";

export default defineNitroConfig({
compatibilityDate: "2024-09-19",
cloudflare: {
wrangler: {
compatibility_flags: ["nodejs_als"],
},
},
});
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
145 changes: 114 additions & 31 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 { existsSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { relative, dirname } from "node:path";
import { writeFile } from "nitropack/kit";
import { parseTOML, stringifyTOML } 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,12 +15,13 @@ 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);
await writeCFWranglerConfig(nitro);
}

export async function writeCFPagesStaticFiles(nitro: Nitro) {
Expand All @@ -35,9 +38,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 @@ -129,7 +133,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,7 +146,7 @@ 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) {
Expand All @@ -169,7 +173,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 +186,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;
async function writeCFWranglerConfig(nitro: Nitro) {
const extraConfig: WranglerConfig = nitro.options.cloudflare?.wrangler || {};

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

// Write wrangler.toml only if config is not empty
if (!inlineConfig || Object.keys(inlineConfig).length === 0) {
// Skip if there are no extra config
if (Object.keys(extraConfig || {}).length === 0) {
return;
}

let configFromFile: WranglerConfig = {} as WranglerConfig;
const configPath = resolve(
nitro.options.rootDir,
inlineConfig.configPath || "wrangler.toml"
);
if (existsSync(configPath)) {
configFromFile = parseTOML<WranglerConfig>(
await fsp.readFile(configPath, "utf8")
// Read user config
const userConfig = await resolveWranglerConfig(nitro.options.rootDir);

// Merge configs
const mergedConfig = userConfig.config
? mergeWranglerConfig(userConfig.config, extraConfig)
: extraConfig;

// Write config
// https://github.com/cloudflare/workers-sdk/pull/7442
const configRedirect = !!process.env.EXPERIMENTAL_WRANGLER_CONFIG;
if (configRedirect) {
if (mergedConfig.pages_build_output_dir) {
throw new Error(
"`pages_build_output_dir` wrangler config should not be set."
);
}
const configPath = join(
Comment on lines +212 to +216

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see that you actively do not want the user to set this.
OK. But in that case you need to provide it automatically.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps add after the check?

    mergedConfig.pages_build_output_dir = nitro.options.output.publicDir;

nitro.options.rootDir,
".wrangler/deploy/config.json"
);
const wranglerConfigPath = join(
nitro.options.output.serverDir,
"wrangler.json"
);
await writeFile(
configPath,
JSON.stringify({
configPath: relative(dirname(configPath), wranglerConfigPath),
}),
true
);
await writeFile(
wranglerConfigPath,
JSON.stringify(mergedConfig, null, 2),
true
);
} else {
// Overwrite user config (TODO: remove when cloudflare/workers-sdk#7442 is GA)
const jsonConfig = join(nitro.options.rootDir, "wrangler.json");
if (existsSync(jsonConfig)) {
await writeFile(jsonConfig, JSON.stringify(mergedConfig, null, 2), true);
} else {
const tomlConfig = join(nitro.options.rootDir, "wrangler.toml");
await writeFile(tomlConfig, stringifyTOML(mergedConfig), true);
}
}
}

const wranglerConfig: WranglerConfig = defu(configFromFile, inlineConfig);

const wranglerPath = join(
isCI ? nitro.options.rootDir : nitro.options.buildDir,
"wrangler.toml"
);
async function resolveWranglerConfig(
dir: string
): Promise<{ path: string; config?: 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,
path: jsonConfig,
};
}
const tomlConfig = join(dir, "wrangler.toml");
if (existsSync(tomlConfig)) {
const config = parseTOML<WranglerConfig>(
await readFile(join(dir, "wrangler.toml"), "utf8")
);
return {
config,
path: tomlConfig,
};
}
return {
path: tomlConfig,
};
}

await fsp.writeFile(wranglerPath, stringifyTOML(wranglerConfig));
/**
* Merge user config with extra config
*
* - Objects/Arrays are merged
* - User config takes precedence over extra config
*/
function mergeWranglerConfig(
userConfig: WranglerConfig = {},
extraConfig: WranglerConfig = {}
): WranglerConfig {
// TODO: Improve logic with explicit merging
const mergedConfig: WranglerConfig = defu(userConfig, extraConfig);
pi0 marked this conversation as resolved.
Show resolved Hide resolved
if (mergedConfig.compatibility_flags) {
mergedConfig.compatibility_flags = [
...new Set(mergedConfig.compatibility_flags || []),
];
if (mergedConfig.compatibility_flags.includes("no_nodejs_compat_v2")) {
mergedConfig.compatibility_flags =
mergedConfig.compatibility_flags.filter(
(flag) => flag !== "nodejs_compat_v2"
);
}
}
return mergedConfig;
}
Loading