@opennextjs/cloudflare 1.16.3 → 1.16.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -34,13 +34,31 @@ const resolver = {
34
34
  type: "core",
35
35
  statusCode: response.status,
36
36
  headers: Object.fromEntries(response.headers.entries()),
37
- // Workers and Node types differ.
38
37
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
- body: response.body || new ReadableStream(),
38
+ body: getResponseBody(method, response),
40
39
  isBase64Encoded: false,
41
40
  };
42
41
  },
43
42
  };
43
+ /**
44
+ * Returns the response body for an asset result.
45
+ *
46
+ * HEAD responses must return `null` because `response.body` is `null` per the HTTP spec
47
+ * and the `new ReadableStream()` fallback would create a stream that never closes, hanging the Worker.
48
+ *
49
+ * @param method - The HTTP method of the request.
50
+ * @param response - The response from the ASSETS binding.
51
+ * @returns The body to use in the internal result.
52
+ */
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ function getResponseBody(method, response) {
55
+ if (method === "HEAD") {
56
+ return null;
57
+ }
58
+ // Workers and Node ReadableStream types differ.
59
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
+ return response.body || new ReadableStream();
61
+ }
44
62
  /**
45
63
  * @param runWorkerFirst `run_worker_first` config
46
64
  * @param pathname pathname of the request
@@ -6,6 +6,7 @@ import * as buildHelper from "@opennextjs/aws/build/helper.js";
6
6
  import { patchOriginalNextConfig } from "@opennextjs/aws/build/patch/patches/index.js";
7
7
  import { printHeader } from "@opennextjs/aws/build/utils.js";
8
8
  import logger from "@opennextjs/aws/logger.js";
9
+ import { ensureNextjsVersionSupported } from "../commands/utils.js";
9
10
  import { bundleServer } from "./bundle-server.js";
10
11
  import { compileCacheAssetsManifestSqlFile } from "./open-next/compile-cache-assets-manifest.js";
11
12
  import { compileEnvFiles } from "./open-next/compile-env-files.js";
@@ -75,18 +76,3 @@ export async function build(options, config, projectOpts, wranglerConfig, allowU
75
76
  await bundleServer(options, projectOpts);
76
77
  logger.info("OpenNext build complete.");
77
78
  }
78
- async function ensureNextjsVersionSupported({ nextVersion }) {
79
- if (buildHelper.compareSemver(nextVersion, "<", "14.2.0")) {
80
- logger.error("Next.js version unsupported, please upgrade to version 14.2 or greater.");
81
- process.exit(1);
82
- }
83
- const { default: { version: wranglerVersion }, } = await import("wrangler/package.json", { with: { type: "json" } });
84
- // We need a version of workerd that has a fix for setImmediate for Next.js 16.1+
85
- // See:
86
- // - https://github.com/cloudflare/workerd/pull/5869
87
- // - https://github.com/opennextjs/opennextjs-cloudflare/issues/1049
88
- if (buildHelper.compareSemver(nextVersion, ">=", "16.1.0") &&
89
- buildHelper.compareSemver(wranglerVersion, "<", "4.59.2")) {
90
- logger.warn(`Next.js 16.1+ requires wrangler 4.59.2 or greater (${wranglerVersion} detected).`);
91
- }
92
- }
@@ -6,6 +6,10 @@
6
6
  *
7
7
  * @param filepath The path to the file.
8
8
  * @param text The text to append to the file.
9
- * @param condition A function that receives the current file content and returns `true` if the text should be appended to it, the condition is skipped when the file is being created.
9
+ * @param opts.appendIf A function that receives the current file content and returns `true` if the text should be appended to it, the condition is skipped when the file is being created. Defaults to a function that always returns true.
10
+ * @param opts.appendPrefix A string that will be inserted between the pre-existing file's content and the new text in case the file already existed. Defaults to an empty string.
10
11
  */
11
- export declare function conditionalAppendFileSync(filepath: string, text: string, condition: (fileContent: string) => boolean): void;
12
+ export declare function conditionalAppendFileSync(filepath: string, text: string, { appendIf, appendPrefix, }?: {
13
+ appendIf?: (fileContent: string) => boolean;
14
+ appendPrefix?: string;
15
+ }): void;
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs";
2
+ import path from "node:path";
2
3
  /**
3
4
  * Appends text to a file
4
5
  *
@@ -7,11 +8,17 @@ import fs from "node:fs";
7
8
  *
8
9
  * @param filepath The path to the file.
9
10
  * @param text The text to append to the file.
10
- * @param condition A function that receives the current file content and returns `true` if the text should be appended to it, the condition is skipped when the file is being created.
11
+ * @param opts.appendIf A function that receives the current file content and returns `true` if the text should be appended to it, the condition is skipped when the file is being created. Defaults to a function that always returns true.
12
+ * @param opts.appendPrefix A string that will be inserted between the pre-existing file's content and the new text in case the file already existed. Defaults to an empty string.
11
13
  */
12
- export function conditionalAppendFileSync(filepath, text, condition) {
14
+ export function conditionalAppendFileSync(filepath, text, { appendIf = () => true, appendPrefix = "", } = {}) {
13
15
  const fileExists = fs.existsSync(filepath);
14
- if (!fileExists || condition(fs.readFileSync(filepath, "utf8"))) {
15
- fs.appendFileSync(filepath, text);
16
+ const maybeFileContent = fileExists ? fs.readFileSync(filepath, "utf8") : "";
17
+ if (!fileExists) {
18
+ const dir = path.dirname(filepath);
19
+ fs.mkdirSync(dir, { recursive: true });
20
+ }
21
+ if (!fileExists || appendIf(maybeFileContent)) {
22
+ fs.appendFileSync(filepath, `${maybeFileContent.length > 0 ? appendPrefix : ""}${text}`);
16
23
  }
17
24
  }
@@ -26,7 +26,7 @@ async function buildCommand(args) {
26
26
  * Consumes 1 positional parameter.
27
27
  */
28
28
  export function addBuildCommand(y) {
29
- return y.command("build", "Build an OpenNext Cloudflare worker", (c) => withWranglerOptions(c)
29
+ return y.command("build [args..]", "Build an OpenNext Cloudflare worker", (c) => withWranglerOptions(c)
30
30
  .option("skipNextBuild", {
31
31
  type: "boolean",
32
32
  alias: ["skipBuild", "s"],
@@ -46,5 +46,5 @@ export async function deployCommand(args) {
46
46
  * Consumes 1 positional parameter.
47
47
  */
48
48
  export function addDeployCommand(y) {
49
- return y.command("deploy", "Deploy a built OpenNext app to Cloudflare Workers", (c) => withPopulateCacheOptions(c), (args) => deployCommand(withWranglerPassthroughArgs(args)));
49
+ return y.command("deploy [args..]", "Deploy a built OpenNext app to Cloudflare Workers", (c) => withPopulateCacheOptions(c), (args) => deployCommand(withWranglerPassthroughArgs(args)));
50
50
  }
@@ -1,12 +1,14 @@
1
+ import assert from "node:assert";
1
2
  import childProcess from "node:child_process";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
4
- import { checkRunningInsideNextjsApp, findNextConfig, findPackagerAndRoot, } from "@opennextjs/aws/build/helper.js";
5
+ import { checkRunningInsideNextjsApp, findNextConfig, findPackagerAndRoot, getNextVersion, } from "@opennextjs/aws/build/helper.js";
5
6
  import logger from "@opennextjs/aws/logger.js";
6
7
  import { conditionalAppendFileSync } from "../build/utils/files.js";
8
+ import { askConfirmation } from "../utils/ask-confirmation.js";
7
9
  import { createOpenNextConfigFile, findOpenNextConfig } from "../utils/open-next-config.js";
8
10
  import { createWranglerConfigFile, findWranglerConfig } from "../utils/wrangler-config.js";
9
- import { printHeaders } from "./utils.js";
11
+ import { ensureNextjsVersionSupported, printHeaders } from "./utils.js";
10
12
  /**
11
13
  * Implementation of the `opennextjs-cloudflare migrate` command.
12
14
  *
@@ -16,6 +18,14 @@ async function migrateCommand(args) {
16
18
  printHeaders("migrate");
17
19
  logger.info("🚀 Setting up the OpenNext Cloudflare adapter...\n");
18
20
  const projectDir = process.cwd();
21
+ const nextConfigFileCreated = await maybeCreateNextConfigFileIfMissing(projectDir, args.forceInstall).catch((e) => {
22
+ logger.error(`${e instanceof Error ? e.message : e}\n`);
23
+ process.exit(1);
24
+ });
25
+ if (nextConfigFileCreated === false) {
26
+ logger.error("The next.config file is required, aborting!\n");
27
+ process.exit(1);
28
+ }
19
29
  checkRunningInsideNextjsApp({ appPath: projectDir });
20
30
  const wranglerConfigFilePath = findWranglerConfig(projectDir);
21
31
  if (wranglerConfigFilePath) {
@@ -49,12 +59,18 @@ async function migrateCommand(args) {
49
59
  await createOpenNextConfigFile("./");
50
60
  const devVarsExists = fs.existsSync(".dev.vars");
51
61
  printStepTitle(`${devVarsExists ? "Updating" : "Creating"} .dev.vars file`);
52
- conditionalAppendFileSync(".dev.vars", "\nNEXTJS_ENV=development\n", (content) => !/\bNEXTJS_ENV\b/.test(content));
62
+ conditionalAppendFileSync(".dev.vars", "NEXTJS_ENV=development\n", {
63
+ appendIf: (content) => !/\bNEXTJS_ENV\b/.test(content),
64
+ appendPrefix: "\n",
65
+ });
53
66
  printStepTitle(`${fs.existsSync("public/_headers") ? "Updating" : "Creating"} public/_headers file`);
54
- conditionalAppendFileSync("public/_headers", "\n\n# https://developers.cloudflare.com/workers/static-assets/headers\n" +
67
+ conditionalAppendFileSync("public/_headers", "# https://developers.cloudflare.com/workers/static-assets/headers\n" +
55
68
  "# https://opennext.js.org/cloudflare/caching#static-assets-caching\n" +
56
69
  "/_next/static/*\n" +
57
- " Cache-Control: public,max-age=31536000,immutable\n\n", (content) => !/^\/_next\/static\/*\b/.test(content));
70
+ " Cache-Control: public,max-age=31536000,immutable\n", {
71
+ appendIf: (content) => !/^\/_next\/static\/*\b/.test(content),
72
+ appendPrefix: "\n\n",
73
+ });
58
74
  printStepTitle("Updating package.json scripts");
59
75
  const openNextScripts = {
60
76
  preview: "opennextjs-cloudflare build && opennextjs-cloudflare preview",
@@ -82,9 +98,19 @@ async function migrateCommand(args) {
82
98
  }
83
99
  const gitIgnoreExists = fs.existsSync(".gitignore");
84
100
  printStepTitle(`${gitIgnoreExists ? "Updating" : "Creating"} .gitignore file`);
85
- conditionalAppendFileSync(".gitignore", "\n# OpenNext\n.open-next\n", (content) => !content.includes(".open-next"));
101
+ conditionalAppendFileSync(".gitignore", "# OpenNext\n.open-next\n", {
102
+ appendIf: (content) => !content.includes(".open-next"),
103
+ appendPrefix: "\n",
104
+ });
105
+ const nextConfig = findNextConfig({ appPath: projectDir });
106
+ // At this point the next config file should exist (it either
107
+ // was part of the original project or we've created it)
108
+ assert(nextConfig, "Next config file unexpectedly missing");
86
109
  printStepTitle("Updating Next.js config");
87
- conditionalAppendFileSync(findNextConfig({ appPath: projectDir }), "\nimport('@opennextjs/cloudflare').then(m => m.initOpenNextCloudflareForDev());\n", (content) => !content.includes("initOpenNextCloudflareForDev"));
110
+ conditionalAppendFileSync(nextConfig.path, "import('@opennextjs/cloudflare').then(m => m.initOpenNextCloudflareForDev());\n", {
111
+ appendIf: (content) => !content.includes("initOpenNextCloudflareForDev"),
112
+ appendPrefix: "\n",
113
+ });
88
114
  printStepTitle("Checking for edge runtime usage");
89
115
  try {
90
116
  const extensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".mts"];
@@ -158,6 +184,53 @@ function findFilesRecursive(dir, extensions, fileList = []) {
158
184
  function printStepTitle(title) {
159
185
  logger.info(`⚙️ ${title}...\n`);
160
186
  }
187
+ /**
188
+ * Creates a plain next.config.ts file
189
+ *
190
+ * @param appDir The directory where the config file should be created
191
+ */
192
+ function createNextConfigFile(appDir) {
193
+ const nextConfigPath = path.join(appDir, "next.config.ts");
194
+ const content = `import type { NextConfig } from "next";
195
+
196
+ const nextConfig: NextConfig = {};
197
+
198
+ export default nextConfig;
199
+ `;
200
+ fs.writeFileSync(nextConfigPath, content);
201
+ }
202
+ /**
203
+ * Creates a next.config.ts file, after asking for the user's confirmation, if missing in the project's directory.
204
+ *
205
+ * To be safe, this function also ensures that the "next" package is installed and its version is compatible with OpenNext.
206
+ *
207
+ * @param projectDir The project directory to check
208
+ * @param skipNextVersionCheck Whether to bypass the "next" version compatibility check
209
+ * @returns A boolean representing whether the user has accepter the creation of the config file, undefined if the file already existed
210
+ * @throws {Error} If "next" is not installed or the Next.js version is incompatible with open-next
211
+ */
212
+ async function maybeCreateNextConfigFileIfMissing(projectDir, skipNextVersionCheck) {
213
+ if (findNextConfig({ appPath: projectDir })) {
214
+ return;
215
+ }
216
+ let nextVersion;
217
+ try {
218
+ nextVersion = getNextVersion(projectDir);
219
+ }
220
+ catch {
221
+ throw new Error("This does not appear to be a Next.js application. The 'next' package is not installed and no next.config file was found.");
222
+ }
223
+ if (!skipNextVersionCheck) {
224
+ await ensureNextjsVersionSupported({ nextVersion });
225
+ }
226
+ const answer = await askConfirmation("Missing required next.config file. Do you want to create one?");
227
+ if (!answer) {
228
+ return false;
229
+ }
230
+ createNextConfigFile(projectDir);
231
+ logger.info("Created next.config.ts\n");
232
+ return true;
233
+ }
161
234
  /**
162
235
  * Add the `migrate` command to yargs configuration.
163
236
  */
@@ -219,8 +219,8 @@ function populateStaticAssetsIncrementalCache(options) {
219
219
  */
220
220
  export function addPopulateCacheCommand(y) {
221
221
  return y.command("populateCache", "Populate the cache for a built Next.js app", (c) => c
222
- .command("local", "Local dev server cache", (c) => withPopulateCacheOptions(c), (args) => populateCacheCommand("local", withWranglerPassthroughArgs(args)))
223
- .command("remote", "Remote Cloudflare Worker cache", (c) => withPopulateCacheOptions(c), (args) => populateCacheCommand("remote", withWranglerPassthroughArgs(args)))
222
+ .command("local [args..]", "Local dev server cache", (c) => withPopulateCacheOptions(c), (args) => populateCacheCommand("local", withWranglerPassthroughArgs(args)))
223
+ .command("remote [args..]", "Remote Cloudflare Worker cache", (c) => withPopulateCacheOptions(c), (args) => populateCacheCommand("remote", withWranglerPassthroughArgs(args)))
224
224
  .demandCommand(1, 1));
225
225
  }
226
226
  export function withPopulateCacheOptions(args) {
@@ -28,7 +28,7 @@ export async function previewCommand(args) {
28
28
  * Consumes 1 positional parameter.
29
29
  */
30
30
  export function addPreviewCommand(y) {
31
- return y.command("preview", "Preview a built OpenNext app with a Wrangler dev server", (c) => withPopulateCacheOptions(c).option("remote", {
31
+ return y.command("preview [args..]", "Preview a built OpenNext app with a Wrangler dev server", (c) => withPopulateCacheOptions(c).option("remote", {
32
32
  type: "boolean",
33
33
  alias: "r",
34
34
  default: false,
@@ -40,5 +40,5 @@ export async function uploadCommand(args) {
40
40
  * Consumes 1 positional parameter.
41
41
  */
42
42
  export function addUploadCommand(y) {
43
- return y.command("upload", "Upload a built OpenNext app to Cloudflare Workers", (c) => withPopulateCacheOptions(c), (args) => uploadCommand(withWranglerPassthroughArgs(args)));
43
+ return y.command("upload [args..]", "Upload a built OpenNext app to Cloudflare Workers", (c) => withPopulateCacheOptions(c), (args) => uploadCommand(withWranglerPassthroughArgs(args)));
44
44
  }
@@ -1,3 +1,4 @@
1
+ import * as buildHelper from "@opennextjs/aws/build/helper.js";
1
2
  import type yargs from "yargs";
2
3
  import type { OpenNextConfig } from "../../api/config.js";
3
4
  export type WithWranglerArgs<T = unknown> = T & {
@@ -12,6 +13,15 @@ export declare const nextAppDir: string;
12
13
  * @param command
13
14
  */
14
15
  export declare function printHeaders(command: string): void;
16
+ /**
17
+ * Validates that the Next.js version is supported and checks wrangler compatibility.
18
+ *
19
+ * Note: this function assumes that wrangler is installed.
20
+ *
21
+ * @param options.nextVersion The detected Next.js version string
22
+ * @throws {Error} If the Next.js version is unsupported
23
+ */
24
+ export declare function ensureNextjsVersionSupported({ nextVersion, }: Pick<buildHelper.BuildOptions, "nextVersion">): Promise<void>;
15
25
  /**
16
26
  * Compile the OpenNext config.
17
27
  *
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import url from "node:url";
5
5
  import { compileOpenNextConfig } from "@opennextjs/aws/build/compileConfig.js";
6
6
  import { normalizeOptions } from "@opennextjs/aws/build/helper.js";
7
+ import * as buildHelper from "@opennextjs/aws/build/helper.js";
7
8
  import { printHeader, showWarningOnWindows } from "@opennextjs/aws/build/utils.js";
8
9
  import logger from "@opennextjs/aws/logger.js";
9
10
  import { unstable_readConfig } from "wrangler";
@@ -18,6 +19,28 @@ export function printHeaders(command) {
18
19
  printHeader(`Cloudflare ${command}`);
19
20
  showWarningOnWindows();
20
21
  }
22
+ /**
23
+ * Validates that the Next.js version is supported and checks wrangler compatibility.
24
+ *
25
+ * Note: this function assumes that wrangler is installed.
26
+ *
27
+ * @param options.nextVersion The detected Next.js version string
28
+ * @throws {Error} If the Next.js version is unsupported
29
+ */
30
+ export async function ensureNextjsVersionSupported({ nextVersion, }) {
31
+ if (buildHelper.compareSemver(nextVersion, "<", "14.2.0")) {
32
+ throw new Error("Next.js version unsupported, please upgrade to version 14.2 or greater.");
33
+ }
34
+ const { default: { version: wranglerVersion }, } = await import("wrangler/package.json", { with: { type: "json" } });
35
+ // We need a version of workerd that has a fix for setImmediate for Next.js 16.1+
36
+ // See:
37
+ // - https://github.com/cloudflare/workerd/pull/5869
38
+ // - https://github.com/opennextjs/opennextjs-cloudflare/issues/1049
39
+ if (buildHelper.compareSemver(nextVersion, ">=", "16.1.0") &&
40
+ buildHelper.compareSemver(wranglerVersion, "<", "4.59.2")) {
41
+ logger.warn(`Next.js 16.1+ requires wrangler 4.59.2 or greater (${wranglerVersion} detected).`);
42
+ }
43
+ }
21
44
  /**
22
45
  * Compile the OpenNext config.
23
46
  *
@@ -120,8 +143,8 @@ function getWranglerArgs(args) {
120
143
  ...(args.config ? ["--config", args.config] : []),
121
144
  ...(args.env ? ["--env", args.env] : []),
122
145
  ...(args.remote ? ["--remote"] : []),
123
- // Note: the first args in `_` will be the commands.
124
- ...args._.slice(args._[0] === "populateCache" ? 2 : 1).map((a) => `${a}`),
146
+ // Note: the `args` array contains unrecognised flags.
147
+ ...(args.args?.map((a) => `${a}`) ?? []),
125
148
  ];
126
149
  }
127
150
  /**
package/dist/cli/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import logger from "@opennextjs/aws/logger.js";
2
3
  import yargs from "yargs";
4
+ import { getVersion } from "./build/utils/version.js";
3
5
  import { addBuildCommand } from "./commands/build.js";
4
6
  import { addDeployCommand } from "./commands/deploy.js";
5
7
  import { addMigrateCommand } from "./commands/migrate.js";
@@ -9,7 +11,22 @@ import { addUploadCommand } from "./commands/upload.js";
9
11
  export function runCommand() {
10
12
  const y = yargs(process.argv.slice(2).filter((arg) => arg !== "--"))
11
13
  .scriptName("opennextjs-cloudflare")
12
- .parserConfiguration({ "unknown-options-as-args": true });
14
+ .parserConfiguration({ "unknown-options-as-args": true })
15
+ .strictCommands()
16
+ .help()
17
+ .alias("h", "help")
18
+ .version(getVersion().cloudflare)
19
+ .alias("v", "version")
20
+ .fail((msg, err, yargs) => {
21
+ if (msg) {
22
+ logger.error(`${msg}\n`);
23
+ }
24
+ if (err) {
25
+ throw err;
26
+ }
27
+ yargs.showHelp();
28
+ process.exit(1);
29
+ });
13
30
  addBuildCommand(y);
14
31
  addPreviewCommand(y);
15
32
  addDeployCommand(y);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@opennextjs/cloudflare",
3
3
  "description": "Cloudflare builder for next apps",
4
- "version": "1.16.3",
4
+ "version": "1.16.5",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "opennextjs-cloudflare": "dist/cli/index.js"
@@ -44,7 +44,7 @@
44
44
  "dependencies": {
45
45
  "@ast-grep/napi": "^0.40.5",
46
46
  "@dotenvx/dotenvx": "1.31.0",
47
- "@opennextjs/aws": "3.9.15",
47
+ "@opennextjs/aws": "3.9.16",
48
48
  "cloudflare": "^4.4.1",
49
49
  "enquirer": "^2.4.1",
50
50
  "glob": "^12.0.0",