diff --git a/.changeset/lazy-balloons-report.md b/.changeset/lazy-balloons-report.md new file mode 100644 index 00000000..72a7a133 --- /dev/null +++ b/.changeset/lazy-balloons-report.md @@ -0,0 +1,9 @@ +--- +"@opennextjs/cloudflare": patch +--- + +check and create a `wrangler.jsonc` file for the user in case a `wrangler.(toml|json|jsonc)` file is not already present + +also introduce a new `--skipWranglerConfigCheck` cli flag and a `SKIP_WRANGLER_CONFIG_CHECK` +environment variable that allows users to opt out of the above check (since developers might +want to use custom locations for their config files) diff --git a/.prettierrc b/.prettierrc index 0adf1369..3c424851 100644 --- a/.prettierrc +++ b/.prettierrc @@ -4,5 +4,14 @@ "semi": true, "useTabs": false, "tabWidth": 2, - "trailingComma": "es5" + "trailingComma": "es5", + "overrides": [ + { + "// comment": "wrangler doesn't seem to accept wrangler.jsonc with trailing commas", + "files": ["**/wrangler.jsonc"], + "options": { + "trailingComma": "none" + } + } + ] } diff --git a/packages/cloudflare/env.d.ts b/packages/cloudflare/env.d.ts index 8303fb30..f7dbe969 100644 --- a/packages/cloudflare/env.d.ts +++ b/packages/cloudflare/env.d.ts @@ -3,6 +3,7 @@ declare global { interface ProcessEnv { __NEXT_PRIVATE_STANDALONE_CONFIG?: string; SKIP_NEXT_APP_BUILD?: string; + SKIP_WRANGLER_CONFIG_CHECK?: string; NEXT_PRIVATE_DEBUG_CACHE?: string; OPEN_NEXT_ORIGIN: string; NODE_ENV?: string; diff --git a/packages/cloudflare/src/cli/args.ts b/packages/cloudflare/src/cli/args.ts index 3dd226b9..f64d0d73 100644 --- a/packages/cloudflare/src/cli/args.ts +++ b/packages/cloudflare/src/cli/args.ts @@ -4,10 +4,11 @@ import { parseArgs } from "node:util"; export function getArgs(): { skipNextBuild: boolean; + skipWranglerConfigCheck: boolean; outputDir?: string; minify: boolean; } { - const { skipBuild, output, noMinify } = parseArgs({ + const { skipBuild, skipWranglerConfigCheck, output, noMinify } = parseArgs({ options: { skipBuild: { type: "boolean", @@ -22,6 +23,10 @@ export function getArgs(): { type: "boolean", default: false, }, + skipWranglerConfigCheck: { + type: "boolean", + default: false, + }, }, allowPositionals: false, }).values; @@ -35,6 +40,9 @@ export function getArgs(): { return { outputDir, skipNextBuild: skipBuild || ["1", "true", "yes"].includes(String(process.env.SKIP_NEXT_APP_BUILD)), + skipWranglerConfigCheck: + skipWranglerConfigCheck || + ["1", "true", "yes"].includes(String(process.env.SKIP_WRANGLER_CONFIG_CHECK)), minify: !noMinify, }; } diff --git a/packages/cloudflare/src/cli/build/index.ts b/packages/cloudflare/src/cli/build/index.ts index 925841e5..ee169dc0 100644 --- a/packages/cloudflare/src/cli/build/index.ts +++ b/packages/cloudflare/src/cli/build/index.ts @@ -1,4 +1,4 @@ -import { cpSync, existsSync } from "node:fs"; +import { cpSync, existsSync, readFileSync, writeFileSync } from "node:fs"; import { createRequire } from "node:module"; import { dirname, join } from "node:path"; @@ -101,6 +101,10 @@ export async function build(projectOpts: ProjectOptions): Promise { // TODO: rely on options only. await bundleServer(projConfig, options); + if (!projectOpts.skipWranglerConfigCheck) { + await createWranglerConfigIfNotExistent(projectOpts); + } + logger.info("OpenNext build complete."); } @@ -178,3 +182,94 @@ function ensureCloudflareConfig(config: OpenNextConfig) { ); } } + +/** + * Creates a `wrangler.jsonc` file for the user if a wrangler config file doesn't already exist, + * but only after asking for the user's confirmation. + * + * If the user refuses a warning is shown (which offers ways to opt out of this check to the user). + * + * @param projectOpts The options for the project + */ +async function createWranglerConfigIfNotExistent(projectOpts: ProjectOptions): Promise { + const possibleExts = ["toml", "json", "jsonc"]; + + const wranglerConfigFileExists = possibleExts.some((ext) => + existsSync(join(projectOpts.sourceDir, `wrangler.${ext}`)) + ); + if (wranglerConfigFileExists) { + return; + } + + const wranglerConfigPath = join(projectOpts.sourceDir, "wrangler.jsonc"); + + const answer = await askConfirmation( + "No `wrangler.(toml|json|jsonc)` config file found, do you want to create one?" + ); + + if (!answer) { + console.warn( + "No Wrangler config file created" + + "\n" + + "(to avoid this check use the `--skipWranglerConfigCheck` flag or set a `SKIP_WRANGLER_CONFIG_CHECK` environment variable to `yes`)" + ); + return; + } + + const wranglerConfigTemplate = readFileSync( + join(getPackageTemplatesDirPath(), "defaults", "wrangler.jsonc"), + "utf8" + ); + let wranglerConfigContent = wranglerConfigTemplate; + + const appName = getAppNameFromPackageJson(projectOpts.sourceDir) ?? "app-name"; + if (appName) { + wranglerConfigContent = wranglerConfigContent.replace( + '"app-name"', + JSON.stringify(appName.replaceAll("_", "-")) + ); + } + + const compatDate = await getLatestCompatDate(); + if (compatDate) { + wranglerConfigContent = wranglerConfigContent.replace( + /"compatibility_date": "\d{4}-\d{2}-\d{2}"/, + `"compatibility_date": ${JSON.stringify(compatDate)}` + ); + } + + writeFileSync(wranglerConfigPath, wranglerConfigContent); +} + +function getAppNameFromPackageJson(sourceDir: string): string | undefined { + try { + const packageJsonStr = readFileSync(join(sourceDir, "package.json"), "utf8"); + const packageJson: Record = JSON.parse(packageJsonStr); + if (typeof packageJson.name === "string") return packageJson.name; + } catch { + /* empty */ + } +} + +export async function getLatestCompatDate(): Promise { + try { + const resp = await fetch(`https://registry.npmjs.org/workerd`); + const latestWorkerdVersion = ( + (await resp.json()) as { + "dist-tags": { latest: string }; + } + )["dist-tags"].latest; + + // The format of the workerd version is `major.yyyymmdd.patch`. + const match = latestWorkerdVersion.match(/\d+\.(\d{4})(\d{2})(\d{2})\.\d+/); + + if (match) { + const [, year, month, date] = match; + const compatDate = `${year}-${month}-${date}`; + + return compatDate; + } + } catch { + /* empty */ + } +} diff --git a/packages/cloudflare/src/cli/config.ts b/packages/cloudflare/src/cli/config.ts index e0888994..6f0884f2 100644 --- a/packages/cloudflare/src/cli/config.ts +++ b/packages/cloudflare/src/cli/config.ts @@ -114,6 +114,8 @@ export type ProjectOptions = { outputDir: string; // Whether the Next.js build should be skipped (i.e. if the `.next` dir is already built) skipNextBuild: boolean; + // Whether the check to see if a wrangler config file exists should be skipped + skipWranglerConfigCheck: boolean; // Whether minification of the worker should be enabled minify: boolean; }; diff --git a/packages/cloudflare/src/cli/index.ts b/packages/cloudflare/src/cli/index.ts index 1993389a..74d96f34 100644 --- a/packages/cloudflare/src/cli/index.ts +++ b/packages/cloudflare/src/cli/index.ts @@ -6,11 +6,12 @@ import { build } from "./build/index.js"; const nextAppDir = process.cwd(); -const { skipNextBuild, outputDir, minify } = getArgs(); +const { skipNextBuild, skipWranglerConfigCheck, outputDir, minify } = getArgs(); await build({ sourceDir: nextAppDir, outputDir: resolve(outputDir ?? nextAppDir, ".open-next"), skipNextBuild, + skipWranglerConfigCheck, minify, }); diff --git a/packages/cloudflare/templates/defaults/wrangler.jsonc b/packages/cloudflare/templates/defaults/wrangler.jsonc new file mode 100644 index 00000000..40ab59b9 --- /dev/null +++ b/packages/cloudflare/templates/defaults/wrangler.jsonc @@ -0,0 +1,18 @@ +{ + "main": ".open-next/worker.js", + "name": "app-name", + "compatibility_date": "2024-12-30", + "compatibility_flags": ["nodejs_compat"], + "assets": { + "directory": ".open-next/assets", + "binding": "ASSETS" + }, + "kv_namespaces": [ + // Create a KV binding with the binding name "NEXT_CACHE_WORKERS_KV" + // to enable the KV based caching: + // { + // "binding": "NEXT_CACHE_WORKERS_KV", + // "id": "" + // } + ] +}