From 335288367d3e82eac12f3bacf23bfc5fe7898981 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Fri, 20 Dec 2024 12:26:41 +0100 Subject: [PATCH 1/5] feat(cloudflare): experimental config redirection support --- playground/nitro.config.ts | 5 + src/presets/cloudflare/types.wrangler.ts | 4 +- src/presets/cloudflare/utils.ts | 133 ++++++++++++++++++----- 3 files changed, 111 insertions(+), 31 deletions(-) diff --git a/playground/nitro.config.ts b/playground/nitro.config.ts index 2d5eb0abf7..ab99046e9b 100644 --- a/playground/nitro.config.ts +++ b/playground/nitro.config.ts @@ -2,4 +2,9 @@ import { defineNitroConfig } from "nitropack/config"; export default defineNitroConfig({ compatibilityDate: "2024-09-19", + cloudflare: { + wrangler: { + compatibility_flags: ["nodejs_als"], + }, + }, }); diff --git a/src/presets/cloudflare/types.wrangler.ts b/src/presets/cloudflare/types.wrangler.ts index a3142da739..09ec175861 100644 --- a/src/presets/cloudflare/types.wrangler.ts +++ b/src/presets/cloudflare/types.wrangler.ts @@ -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 & PagesConfigFields & Environment; +export type Config = Partial< + ConfigFields & PagesConfigFields & Environment +>; export type RawConfig = Partial> & PagesConfigFields & diff --git a/src/presets/cloudflare/utils.ts b/src/presets/cloudflare/utils.ts index b12cc93d7f..b6245e6e05 100644 --- a/src/presets/cloudflare/utils.ts +++ b/src/presets/cloudflare/utils.ts @@ -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, @@ -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) { @@ -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) { @@ -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)." @@ -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) { @@ -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)." @@ -182,37 +186,106 @@ 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( - 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; + + // Explicitly fail if pages_build_output_dir is set + if (mergedConfig.pages_build_output_dir) { + throw new Error( + "Custom wrangler `pages_build_output_dir` is not supported." ); } - const wranglerConfig: WranglerConfig = defu(configFromFile, inlineConfig); + // Write config + // https://github.com/cloudflare/workers-sdk/pull/7442 + const configRedirect = !!process.env.EXPERIMENTAL_WRANGLER_CONFIG; + if (configRedirect) { + const configPath = join( + 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 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( + 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 + return defu(userConfig, extraConfig); } From 71fd0c579ef3477b4ccbc16a1db86955d1dcd7dc Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Fri, 20 Dec 2024 14:08:26 +0100 Subject: [PATCH 2/5] dedup compatibility_flags --- src/presets/cloudflare/utils.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/presets/cloudflare/utils.ts b/src/presets/cloudflare/utils.ts index b6245e6e05..ccf85e11f8 100644 --- a/src/presets/cloudflare/utils.ts +++ b/src/presets/cloudflare/utils.ts @@ -287,5 +287,12 @@ function mergeWranglerConfig( extraConfig: WranglerConfig = {} ): WranglerConfig { // TODO: Improve logic with explicit merging - return defu(userConfig, extraConfig); + const mergedConfig: WranglerConfig = defu(userConfig, extraConfig); + if (mergedConfig.compatibility_flags) { + // TODO: exclude `no_` configs from userConfig + mergedConfig.compatibility_flags = [ + ...new Set(mergedConfig.compatibility_flags), + ]; + } + return mergedConfig; } From 9aca49f653edc5fe9714a3490b9f310a89774755 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Fri, 20 Dec 2024 14:11:28 +0100 Subject: [PATCH 3/5] respect `no_nodejs_compat_v2` --- src/presets/cloudflare/utils.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/presets/cloudflare/utils.ts b/src/presets/cloudflare/utils.ts index ccf85e11f8..244c0c652b 100644 --- a/src/presets/cloudflare/utils.ts +++ b/src/presets/cloudflare/utils.ts @@ -289,10 +289,15 @@ function mergeWranglerConfig( // TODO: Improve logic with explicit merging const mergedConfig: WranglerConfig = defu(userConfig, extraConfig); if (mergedConfig.compatibility_flags) { - // TODO: exclude `no_` configs from userConfig 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 !== "no_nodejs_compat_v2" + ); + } } return mergedConfig; } From fc9dcec13f6d8eef5a4b4cd718d291423aeba286 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Fri, 20 Dec 2024 14:12:20 +0100 Subject: [PATCH 4/5] up --- src/presets/cloudflare/utils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/presets/cloudflare/utils.ts b/src/presets/cloudflare/utils.ts index 244c0c652b..afa8c96c82 100644 --- a/src/presets/cloudflare/utils.ts +++ b/src/presets/cloudflare/utils.ts @@ -290,12 +290,12 @@ function mergeWranglerConfig( const mergedConfig: WranglerConfig = defu(userConfig, extraConfig); if (mergedConfig.compatibility_flags) { mergedConfig.compatibility_flags = [ - ...new Set(mergedConfig.compatibility_flags), + ...new Set(mergedConfig.compatibility_flags || []), ]; - if (mergedConfig.compatibility_flags.includes?.("no_nodejs_compat_v2")) { + if (mergedConfig.compatibility_flags.includes("no_nodejs_compat_v2")) { mergedConfig.compatibility_flags = mergedConfig.compatibility_flags.filter( - (flag) => flag !== "no_nodejs_compat_v2" + (flag) => flag !== "nodejs_compat_v2" ); } } From 948be84df82a4ac151c497b64364be916e60d155 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Fri, 20 Dec 2024 14:15:04 +0100 Subject: [PATCH 5/5] update err --- src/presets/cloudflare/utils.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/presets/cloudflare/utils.ts b/src/presets/cloudflare/utils.ts index afa8c96c82..48dd340717 100644 --- a/src/presets/cloudflare/utils.ts +++ b/src/presets/cloudflare/utils.ts @@ -205,17 +205,15 @@ async function writeCFWranglerConfig(nitro: Nitro) { ? mergeWranglerConfig(userConfig.config, extraConfig) : extraConfig; - // Explicitly fail if pages_build_output_dir is set - if (mergedConfig.pages_build_output_dir) { - throw new Error( - "Custom wrangler `pages_build_output_dir` is not supported." - ); - } - // 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( nitro.options.rootDir, ".wrangler/deploy/config.json"