@opennextjs/cloudflare 1.7.0 → 1.8.0

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.
@@ -1,3 +1,4 @@
1
+ import { error } from "@opennextjs/aws/adapters/logger.js";
1
2
  import { getCloudflareContext } from "../../cloudflare-context.js";
2
3
  import { debugCache, internalPurgeCacheByTags } from "../internal.js";
3
4
  export const purgeCache = ({ type = "direct" }) => {
@@ -13,7 +14,7 @@ export const purgeCache = ({ type = "direct" }) => {
13
14
  else {
14
15
  const durableObject = env.NEXT_CACHE_DO_PURGE;
15
16
  if (!durableObject) {
16
- debugCache("cdnInvalidation", "No durable object found. Skipping cache purge.");
17
+ error("Purge cache: NEXT_CACHE_DO_PURGE not found. Skipping cache purge.");
17
18
  return;
18
19
  }
19
20
  const id = durableObject.idFromName("cache-purge");
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { error } from "@opennextjs/aws/adapters/logger.js";
2
3
  import { getCloudflareContext } from "../cloudflare-context.js";
3
4
  export const debugCache = (name, ...args) => {
4
5
  if (process.env.NEXT_PRIVATE_DEBUG_CACHE) {
@@ -29,9 +30,9 @@ export async function purgeCacheByTags(tags) {
29
30
  }
30
31
  }
31
32
  export async function internalPurgeCacheByTags(env, tags) {
32
- if (!env.CACHE_PURGE_ZONE_ID && !env.CACHE_PURGE_API_TOKEN) {
33
+ if (!env.CACHE_PURGE_ZONE_ID || !env.CACHE_PURGE_API_TOKEN) {
33
34
  // THIS IS A NO-OP
34
- debugCache("purgeCacheByTags", "No cache zone ID or API token provided. Skipping cache purge.");
35
+ error("No cache zone ID or API token provided. Skipping cache purge.");
35
36
  return "missing-credentials";
36
37
  }
37
38
  let response;
@@ -48,12 +49,12 @@ export async function internalPurgeCacheByTags(env, tags) {
48
49
  });
49
50
  if (response.status === 429) {
50
51
  // Rate limit exceeded
51
- debugCache("purgeCacheByTags", "Rate limit exceeded. Skipping cache purge.");
52
+ error("purgeCacheByTags: Rate limit exceeded. Skipping cache purge.");
52
53
  return "rate-limit-exceeded";
53
54
  }
54
55
  const bodyResponse = (await response.json());
55
56
  if (!bodyResponse.success) {
56
- debugCache("purgeCacheByTags", "Cache purge failed. Errors:", bodyResponse.errors.map((error) => `${error.code}: ${error.message}`));
57
+ error("purgeCacheByTags: Cache purge failed. Errors:", bodyResponse.errors.map((error) => `${error.code}: ${error.message}`));
57
58
  return "purge-failed";
58
59
  }
59
60
  debugCache("purgeCacheByTags", "Cache purged successfully for tags:", tags);
@@ -22,3 +22,23 @@ export declare const buildIdRule = "\nrule:\n pattern:\n selector: method_de
22
22
  */
23
23
  export declare function createCacheHandlerRule(handlerPath: string): string;
24
24
  export declare function createComposableCacheHandlersRule(handlerPath: string): string;
25
+ /**
26
+ * `attachRequestMeta` sets `initUrl` to always be with `https` cause this.fetchHostname && this.port is undefined in our case.
27
+ * this.nextConfig.experimental.trustHostHeader is also true.
28
+ *
29
+ * This patch checks if the original protocol was "http:" and rewrites the `initUrl` to reflect the actual host protocol.
30
+ * It will make `request.url` in route handlers end up with the correct protocol.
31
+ *
32
+ * Note: We cannot use the already defined `initURL` we passed in as requestMetaData to NextServer's request handler as pages router
33
+ * data routes would fail. It would miss the `_next/data` part in the path in that case.
34
+ *
35
+ * Therefor we just replace the protocol if necessary in the value from this template string:
36
+ * https://github.com/vercel/next.js/blob/ea08bf27/packages/next/src/server/next-server.ts#L1920
37
+ *
38
+ * Affected lines:
39
+ * https://github.com/vercel/next.js/blob/ea08bf27/packages/next/src/server/next-server.ts#L1916-L1923
40
+ *
41
+ * Callstack: handleRequest-> handleRequestImpl -> attachRequestMeta
42
+ *
43
+ */
44
+ export declare const attachRequestMetaRule = "\nrule:\n kind: identifier\n regex: ^initUrl$\n inside:\n kind: arguments\n all:\n - has: {kind: identifier, regex: ^req$}\n - has: {kind: string, regex: initURL}\n inside:\n kind: call_expression\n all:\n - has: {kind: parenthesized_expression, regex: '0'}\n - has: { regex: _requestmeta.addRequestMeta}\n inside:\n kind: expression_statement\n inside:\n kind: statement_block\n inside:\n kind: method_definition\n has:\n kind: property_identifier\n regex: ^attachRequestMeta$\nfix:\n 'req[Symbol.for(\"NextInternalRequestMeta\")]?.initProtocol === \"http:\" && initUrl.startsWith(\"https://\") ? `http://${initUrl.slice(8)}`: initUrl'";
@@ -28,6 +28,7 @@ export function patchNextServer(updater, buildOpts) {
28
28
  contents = patchCode(contents, createComposableCacheHandlersRule(composableCacheHandler));
29
29
  // Node middleware are not supported on Cloudflare yet
30
30
  contents = patchCode(contents, disableNodeMiddlewareRule);
31
+ contents = patchCode(contents, attachRequestMetaRule);
31
32
  return contents;
32
33
  },
33
34
  },
@@ -100,3 +101,47 @@ fix: |-
100
101
  globalThis[handlersSetSymbol] = new Set(globalThis[handlersMapSymbol].values());
101
102
  `;
102
103
  }
104
+ /**
105
+ * `attachRequestMeta` sets `initUrl` to always be with `https` cause this.fetchHostname && this.port is undefined in our case.
106
+ * this.nextConfig.experimental.trustHostHeader is also true.
107
+ *
108
+ * This patch checks if the original protocol was "http:" and rewrites the `initUrl` to reflect the actual host protocol.
109
+ * It will make `request.url` in route handlers end up with the correct protocol.
110
+ *
111
+ * Note: We cannot use the already defined `initURL` we passed in as requestMetaData to NextServer's request handler as pages router
112
+ * data routes would fail. It would miss the `_next/data` part in the path in that case.
113
+ *
114
+ * Therefor we just replace the protocol if necessary in the value from this template string:
115
+ * https://github.com/vercel/next.js/blob/ea08bf27/packages/next/src/server/next-server.ts#L1920
116
+ *
117
+ * Affected lines:
118
+ * https://github.com/vercel/next.js/blob/ea08bf27/packages/next/src/server/next-server.ts#L1916-L1923
119
+ *
120
+ * Callstack: handleRequest-> handleRequestImpl -> attachRequestMeta
121
+ *
122
+ */
123
+ export const attachRequestMetaRule = `
124
+ rule:
125
+ kind: identifier
126
+ regex: ^initUrl$
127
+ inside:
128
+ kind: arguments
129
+ all:
130
+ - has: {kind: identifier, regex: ^req$}
131
+ - has: {kind: string, regex: initURL}
132
+ inside:
133
+ kind: call_expression
134
+ all:
135
+ - has: {kind: parenthesized_expression, regex: '0'}
136
+ - has: { regex: _requestmeta.addRequestMeta}
137
+ inside:
138
+ kind: expression_statement
139
+ inside:
140
+ kind: statement_block
141
+ inside:
142
+ kind: method_definition
143
+ has:
144
+ kind: property_identifier
145
+ regex: ^attachRequestMeta$
146
+ fix:
147
+ 'req[Symbol.for("NextInternalRequestMeta")]?.initProtocol === "http:" && initUrl.startsWith("https://") ? \`http://\${initUrl.slice(8)}\`: initUrl'`;
@@ -15,5 +15,6 @@ export declare function getLatestCompatDate(): Promise<string | undefined>;
15
15
  * If the user refuses an error is thrown (since the file is mandatory).
16
16
  *
17
17
  * @param sourceDir The source directory for the project
18
+ * @return The path to the created source file
18
19
  */
19
- export declare function createOpenNextConfigIfNotExistent(sourceDir: string): Promise<void>;
20
+ export declare function createOpenNextConfigIfNotExistent(sourceDir: string): Promise<string>;
@@ -67,6 +67,7 @@ export async function getLatestCompatDate() {
67
67
  * If the user refuses an error is thrown (since the file is mandatory).
68
68
  *
69
69
  * @param sourceDir The source directory for the project
70
+ * @return The path to the created source file
70
71
  */
71
72
  export async function createOpenNextConfigIfNotExistent(sourceDir) {
72
73
  const openNextConfigPath = join(sourceDir, "open-next.config.ts");
@@ -77,4 +78,5 @@ export async function createOpenNextConfigIfNotExistent(sourceDir) {
77
78
  }
78
79
  cpSync(join(getPackageTemplatesDirPath(), "open-next.config.ts"), openNextConfigPath);
79
80
  }
81
+ return openNextConfigPath;
80
82
  }
@@ -7,7 +7,7 @@ import { compileConfig, getNormalizedOptions, nextAppDir, printHeaders, readWran
7
7
  */
8
8
  async function buildCommand(args) {
9
9
  printHeaders("build");
10
- const { config, buildDir } = await compileConfig();
10
+ const { config, buildDir } = await compileConfig(args.openNextConfigPath);
11
11
  const options = getNormalizedOptions(config, buildDir);
12
12
  const wranglerConfig = readWranglerConfig(args);
13
13
  await buildImpl(options, config, { ...args, minify: !args.noMinify, sourceDir: nextAppDir }, wranglerConfig);
@@ -34,5 +34,9 @@ export function addBuildCommand(y) {
34
34
  type: "boolean",
35
35
  default: ["1", "true", "yes"].includes(String(process.env.SKIP_WRANGLER_CONFIG_CHECK)),
36
36
  desc: "Skip checking for a Wrangler config",
37
+ })
38
+ .option("openNextConfigPath", {
39
+ type: "string",
40
+ desc: "Path to the OpenNext configuration file",
37
41
  }), (args) => buildCommand(withWranglerPassthroughArgs(args)));
38
42
  }
@@ -15,14 +15,14 @@ export async function deployCommand(args) {
15
15
  const options = getNormalizedOptions(config);
16
16
  const wranglerConfig = readWranglerConfig(args);
17
17
  const envVars = await getEnvFromPlatformProxy({
18
- configPath: args.configPath,
18
+ configPath: args.wranglerConfigPath,
19
19
  environment: args.env,
20
20
  });
21
21
  const deploymentMapping = await getDeploymentMapping(options, config, envVars);
22
22
  await populateCache(options, config, wranglerConfig, {
23
23
  target: "remote",
24
24
  environment: args.env,
25
- configPath: args.configPath,
25
+ wranglerConfigPath: args.wranglerConfigPath,
26
26
  cacheChunkSize: args.cacheChunkSize,
27
27
  });
28
28
  runWrangler(options, [
@@ -13,7 +13,7 @@ export declare function getCacheAssets(opts: BuildOptions): CacheAsset[];
13
13
  type PopulateCacheOptions = {
14
14
  target: WranglerTarget;
15
15
  environment?: string;
16
- configPath?: string;
16
+ wranglerConfigPath?: string;
17
17
  cacheChunkSize?: number;
18
18
  };
19
19
  export declare function populateCache(options: BuildOptions, config: OpenNextConfig, wranglerConfig: WranglerConfig, populateCacheOptions: PopulateCacheOptions): Promise<void>;
@@ -24,6 +24,8 @@ export declare function populateCache(options: BuildOptions, config: OpenNextCon
24
24
  */
25
25
  export declare function addPopulateCacheCommand<T extends yargs.Argv>(y: T): yargs.Argv<{}>;
26
26
  export declare function withPopulateCacheOptions<T extends yargs.Argv>(args: T): yargs.Argv<{
27
+ config: string | undefined;
28
+ } & {
27
29
  configPath: string | undefined;
28
30
  } & {
29
31
  env: string | undefined;
@@ -76,7 +76,7 @@ async function populateR2IncrementalCache(options, config, populateCacheOptions)
76
76
  `--file ${quoteShellMeta(fullPath)}`,
77
77
  ], {
78
78
  target: populateCacheOptions.target,
79
- configPath: populateCacheOptions.configPath,
79
+ configPath: populateCacheOptions.wranglerConfigPath,
80
80
  // R2 does not support the environment flag and results in the following error:
81
81
  // Incorrect type for the 'cacheExpiry' field on 'HttpMetadata': the provided value is not of type 'date'.
82
82
  environment: undefined,
@@ -113,7 +113,7 @@ async function populateKVIncrementalCache(options, config, populateCacheOptions)
113
113
  runWrangler(options, ["kv bulk put", quoteShellMeta(chunkPath), `--binding ${KV_CACHE_BINDING_NAME}`], {
114
114
  target: populateCacheOptions.target,
115
115
  environment: populateCacheOptions.environment,
116
- configPath: populateCacheOptions.configPath,
116
+ configPath: populateCacheOptions.wranglerConfigPath,
117
117
  logging: "error",
118
118
  });
119
119
  rmSync(chunkPath);
@@ -133,7 +133,7 @@ function populateD1TagCache(options, config, populateCacheOptions) {
133
133
  ], {
134
134
  target: populateCacheOptions.target,
135
135
  environment: populateCacheOptions.environment,
136
- configPath: populateCacheOptions.configPath,
136
+ configPath: populateCacheOptions.wranglerConfigPath,
137
137
  logging: "error",
138
138
  });
139
139
  logger.info("\nSuccessfully created D1 table");
@@ -189,7 +189,7 @@ async function populateCacheCommand(target, args) {
189
189
  await populateCache(options, config, wranglerConfig, {
190
190
  target,
191
191
  environment: args.env,
192
- configPath: args.configPath,
192
+ wranglerConfigPath: args.wranglerConfigPath,
193
193
  cacheChunkSize: args.cacheChunkSize,
194
194
  });
195
195
  }
@@ -14,7 +14,7 @@ export async function previewCommand(args) {
14
14
  await populateCache(options, config, wranglerConfig, {
15
15
  target: "local",
16
16
  environment: args.env,
17
- configPath: args.configPath,
17
+ wranglerConfigPath: args.wranglerConfigPath,
18
18
  cacheChunkSize: args.cacheChunkSize,
19
19
  });
20
20
  runWrangler(options, ["dev", ...args.wranglerArgs], { logging: "all" });
@@ -15,14 +15,14 @@ export async function uploadCommand(args) {
15
15
  const options = getNormalizedOptions(config);
16
16
  const wranglerConfig = readWranglerConfig(args);
17
17
  const envVars = await getEnvFromPlatformProxy({
18
- configPath: args.configPath,
18
+ configPath: args.wranglerConfigPath,
19
19
  environment: args.env,
20
20
  });
21
21
  const deploymentMapping = await getDeploymentMapping(options, config, envVars);
22
22
  await populateCache(options, config, wranglerConfig, {
23
23
  target: "remote",
24
24
  environment: args.env,
25
- configPath: args.configPath,
25
+ wranglerConfigPath: args.wranglerConfigPath,
26
26
  cacheChunkSize: args.cacheChunkSize,
27
27
  });
28
28
  runWrangler(options, [
@@ -2,7 +2,7 @@ import type yargs from "yargs";
2
2
  import type { OpenNextConfig } from "../../api/config.js";
3
3
  export type WithWranglerArgs<T = unknown> = T & {
4
4
  wranglerArgs: string[];
5
- configPath: string | undefined;
5
+ wranglerConfigPath: string | undefined;
6
6
  env: string | undefined;
7
7
  };
8
8
  export declare const nextAppDir: string;
@@ -13,11 +13,17 @@ export declare const nextAppDir: string;
13
13
  */
14
14
  export declare function printHeaders(command: string): void;
15
15
  /**
16
- * Compile the OpenNext config, and ensure it is for Cloudflare.
16
+ * Compile the OpenNext config.
17
+ *
18
+ * When users do not specify a custom config file (using `--openNextConfigPath`),
19
+ * the CLI will offer to create one.
20
+ *
21
+ * When users specify a custom config file but it doesn't exist, we throw an Error.
17
22
  *
23
+ * @param configPath Optional path to the config file. Absolute or relative to cwd.
18
24
  * @returns OpenNext config.
19
25
  */
20
- export declare function compileConfig(): Promise<{
26
+ export declare function compileConfig(configPath: string | undefined): Promise<{
21
27
  config: import("@opennextjs/aws/types/open-next.js").OpenNextConfig;
22
28
  buildDir: string;
23
29
  }>;
@@ -64,18 +70,21 @@ export declare function readWranglerConfig(args: WithWranglerArgs): import("wran
64
70
  * Adds flags for the wrangler config path and environment to the yargs configuration.
65
71
  */
66
72
  export declare function withWranglerOptions<T extends yargs.Argv>(args: T): yargs.Argv<{
73
+ config: string | undefined;
74
+ } & {
67
75
  configPath: string | undefined;
68
76
  } & {
69
77
  env: string | undefined;
70
78
  }>;
79
+ type WranglerInputArgs = {
80
+ configPath: string | undefined;
81
+ config: string | undefined;
82
+ env: string | undefined;
83
+ };
71
84
  /**
72
85
  *
73
86
  * @param args
74
87
  * @returns The inputted args, and an array of arguments that can be given to wrangler commands, including the `--config` and `--env` args.
75
88
  */
76
- export declare function withWranglerPassthroughArgs<T extends yargs.ArgumentsCamelCase<{
77
- configPath: string | undefined;
78
- env: string | undefined;
79
- }>>(args: T): T & {
80
- wranglerArgs: string[];
81
- };
89
+ export declare function withWranglerPassthroughArgs<T extends yargs.ArgumentsCamelCase<WranglerInputArgs>>(args: T): WithWranglerArgs<T>;
90
+ export {};
@@ -18,13 +18,25 @@ export function printHeaders(command) {
18
18
  showWarningOnWindows();
19
19
  }
20
20
  /**
21
- * Compile the OpenNext config, and ensure it is for Cloudflare.
21
+ * Compile the OpenNext config.
22
22
  *
23
+ * When users do not specify a custom config file (using `--openNextConfigPath`),
24
+ * the CLI will offer to create one.
25
+ *
26
+ * When users specify a custom config file but it doesn't exist, we throw an Error.
27
+ *
28
+ * @param configPath Optional path to the config file. Absolute or relative to cwd.
23
29
  * @returns OpenNext config.
24
30
  */
25
- export async function compileConfig() {
26
- await createOpenNextConfigIfNotExistent(nextAppDir);
27
- const { config, buildDir } = await compileOpenNextConfig(nextAppDir, undefined, { compileEdge: true });
31
+ export async function compileConfig(configPath) {
32
+ if (configPath && !existsSync(configPath)) {
33
+ throw new Error(`Custom config file not found at ${configPath}`);
34
+ }
35
+ if (!configPath) {
36
+ configPath = await createOpenNextConfigIfNotExistent(nextAppDir);
37
+ }
38
+ // TODO: remove the hack passing the `configPath` as the `baseDir` when https://github.com/opennextjs/opennextjs-aws/pull/972 is merged
39
+ const { config, buildDir } = await compileOpenNextConfig(configPath, "", { compileEdge: true });
28
40
  ensureCloudflareConfig(config);
29
41
  return { config, buildDir };
30
42
  }
@@ -64,19 +76,24 @@ export function getNormalizedOptions(config, buildDir = nextAppDir) {
64
76
  * @returns Wrangler config.
65
77
  */
66
78
  export function readWranglerConfig(args) {
67
- return unstable_readConfig({ env: args.env, config: args.configPath });
79
+ return unstable_readConfig({ env: args.env, config: args.wranglerConfigPath });
68
80
  }
69
81
  /**
70
82
  * Adds flags for the wrangler config path and environment to the yargs configuration.
71
83
  */
72
84
  export function withWranglerOptions(args) {
73
85
  return args
74
- .options("configPath", {
86
+ .option("config", {
75
87
  type: "string",
76
- alias: ["config", "c"],
88
+ alias: "c",
77
89
  desc: "Path to Wrangler configuration file",
78
90
  })
79
- .options("env", {
91
+ .option("configPath", {
92
+ type: "string",
93
+ desc: "Path to Wrangler configuration file",
94
+ deprecated: true,
95
+ })
96
+ .option("env", {
80
97
  type: "string",
81
98
  alias: "e",
82
99
  desc: "Wrangler environment to use for operations",
@@ -88,8 +105,16 @@ export function withWranglerOptions(args) {
88
105
  * @returns An array of arguments that can be given to wrangler commands, including the `--config` and `--env` args.
89
106
  */
90
107
  function getWranglerArgs(args) {
108
+ if (args.configPath) {
109
+ logger.warn("The `--configPath` flag is deprecated, please use `--config` instead.");
110
+ if (args.config) {
111
+ logger.error("Multiple config flags found. Please use the `--config` flag for your Wrangler config path.");
112
+ process.exit(1);
113
+ }
114
+ }
91
115
  return [
92
116
  ...(args.configPath ? ["--config", args.configPath] : []),
117
+ ...(args.config ? ["--config", args.config] : []),
93
118
  ...(args.env ? ["--env", args.env] : []),
94
119
  // Note: the first args in `_` will be the commands.
95
120
  ...args._.slice(args._[0] === "populateCache" ? 2 : 1).map((a) => `${a}`),
@@ -101,5 +126,9 @@ function getWranglerArgs(args) {
101
126
  * @returns The inputted args, and an array of arguments that can be given to wrangler commands, including the `--config` and `--env` args.
102
127
  */
103
128
  export function withWranglerPassthroughArgs(args) {
104
- return { ...args, wranglerArgs: getWranglerArgs(args) };
129
+ return {
130
+ ...args,
131
+ wranglerConfigPath: args.config ?? args.configPath,
132
+ wranglerArgs: getWranglerArgs(args),
133
+ };
105
134
  }
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.7.0",
4
+ "version": "1.8.0",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "opennextjs-cloudflare": "dist/cli/index.js"