@@ -45,6 +45,14 @@ type InternalGlobalThis<
45
45
__NEXT_DATA__ : Record < string , unknown > ;
46
46
} ;
47
47
48
+ type GetCloudflareContextOptions = {
49
+ /**
50
+ * When `true`, `getCloudflareContext` returns a promise of the cloudflare context instead of the context,
51
+ * this is needed to access the context from statically generated routes.
52
+ */
53
+ async : boolean ;
54
+ } ;
55
+
48
56
/**
49
57
* Utility to get the current Cloudflare context
50
58
*
@@ -53,44 +61,100 @@ type InternalGlobalThis<
53
61
export function getCloudflareContext <
54
62
CfProperties extends Record < string , unknown > = IncomingRequestCfProperties ,
55
63
Context = ExecutionContext ,
56
- > ( ) : CloudflareContext < CfProperties , Context > {
64
+ > ( options : { async : true } ) : Promise < CloudflareContext < CfProperties , Context > > ;
65
+ export function getCloudflareContext <
66
+ CfProperties extends Record < string , unknown > = IncomingRequestCfProperties ,
67
+ Context = ExecutionContext ,
68
+ > ( options ?: { async : false } ) : CloudflareContext < CfProperties , Context > ;
69
+ export function getCloudflareContext <
70
+ CfProperties extends Record < string , unknown > = IncomingRequestCfProperties ,
71
+ Context = ExecutionContext ,
72
+ > (
73
+ options : GetCloudflareContextOptions = { async : false }
74
+ ) : CloudflareContext < CfProperties , Context > | Promise < CloudflareContext < CfProperties , Context > > {
75
+ return options . async ? getCloudflareContextAsync ( ) : getCloudflareContextSync ( ) ;
76
+ }
77
+
78
+ /**
79
+ * Get the cloudflare context from the current global scope
80
+ */
81
+ function getCloudflareContextFromGlobalScope <
82
+ CfProperties extends Record < string , unknown > = IncomingRequestCfProperties ,
83
+ Context = ExecutionContext ,
84
+ > ( ) : CloudflareContext < CfProperties , Context > | undefined {
85
+ const global = globalThis as InternalGlobalThis < CfProperties , Context > ;
86
+ return global [ cloudflareContextSymbol ] ;
87
+ }
88
+
89
+ /**
90
+ * Detects whether the current code is being evaluated in a statically generated route
91
+ */
92
+ function inSSG <
93
+ CfProperties extends Record < string , unknown > = IncomingRequestCfProperties ,
94
+ Context = ExecutionContext ,
95
+ > ( ) : boolean {
57
96
const global = globalThis as InternalGlobalThis < CfProperties , Context > ;
97
+ // Note: Next.js sets globalThis.__NEXT_DATA__.nextExport to true for SSG routes
98
+ // source: https://github.com/vercel/next.js/blob/4e394608423/packages/next/src/export/worker.ts#L55-L57)
99
+ return global . __NEXT_DATA__ ?. nextExport === true ;
100
+ }
58
101
59
- const cloudflareContext = global [ cloudflareContextSymbol ] ;
60
-
61
- if ( ! cloudflareContext ) {
62
- // For SSG Next.js creates (jest) workers that run in parallel, those don't get the current global
63
- // state so they can't get access to the cloudflare context, unfortunately there isn't anything we
64
- // can do about this, so the only solution is to error asking the developer to opt-out of SSG
65
- // Next.js sets globalThis.__NEXT_DATA__.nextExport to true for the worker, so we can use that to detect
66
- // that the route is being SSG'd (source: https://github.com/vercel/next.js/blob/4e394608423/packages/next/src/export/worker.ts#L55-L57)
67
- if ( global . __NEXT_DATA__ ?. nextExport === true ) {
68
- throw new Error (
69
- `\n\nERROR: \`getCloudflareContext\` has been called in a static route` +
70
- ` that is not allowed, please either avoid calling \`getCloudflareContext\`` +
71
- ` in the route or make the route non static (for example by exporting the` +
72
- ` \`dynamic\` route segment config set to \`'force-dynamic'\`.\n`
73
- ) ;
74
- }
75
-
76
- // the cloudflare context is initialized by the worker and is always present in production/preview
77
- // during local development (`next dev`) it might be missing only if the developers hasn't called
78
- // the `initOpenNextCloudflareForDev` function in their Next.js config file
102
+ /**
103
+ * Utility to get the current Cloudflare context in sync mode
104
+ */
105
+ function getCloudflareContextSync <
106
+ CfProperties extends Record < string , unknown > = IncomingRequestCfProperties ,
107
+ Context = ExecutionContext ,
108
+ > ( ) : CloudflareContext < CfProperties , Context > {
109
+ const cloudflareContext = getCloudflareContextFromGlobalScope < CfProperties , Context > ( ) ;
110
+
111
+ if ( cloudflareContext ) {
112
+ return cloudflareContext ;
113
+ }
114
+
115
+ // The sync mode of `getCloudflareContext`, relies on the context being set on the global state
116
+ // by either the worker entrypoint (in prod) or by `initOpenNextCloudflareForDev` (in dev), neither
117
+ // can work during SSG since for SSG Next.js creates (jest) workers that don't get access to the
118
+ // normal global state so we throw with a helpful error message.
119
+ if ( inSSG ( ) ) {
79
120
throw new Error (
80
- `\n\nERROR: \`getCloudflareContext\` has been called without having called` +
81
- ` \`initOpenNextCloudflareForDev\` from the Next.js config file.\n` +
82
- `You should update your Next.js config file as shown below:\n\n` +
83
- " ```\n // next.config.mjs\n\n" +
84
- ` import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";\n\n` +
85
- ` initOpenNextCloudflareForDev();\n\n` +
86
- " const nextConfig = { ... };\n" +
87
- " export default nextConfig;\n" +
88
- " ```\n" +
89
- "\n"
121
+ `\n\nERROR: \`getCloudflareContext\` has been called in a static route,` +
122
+ ` that is not allowed, this can be solved in different ways:\n\n` +
123
+ ` - call \`getCloudflareContext({async: true})\` to use the \`async\` mode\n` +
124
+ ` - avoid calling \`getCloudflareContext\` in the route\n` +
125
+ ` - make the route non static\n`
90
126
) ;
91
127
}
92
128
93
- return cloudflareContext ;
129
+ throw new Error ( initOpenNextCloudflareForDevErrorMsg ) ;
130
+ }
131
+
132
+ /**
133
+ * Utility to get the current Cloudflare context in async mode
134
+ */
135
+ async function getCloudflareContextAsync <
136
+ CfProperties extends Record < string , unknown > = IncomingRequestCfProperties ,
137
+ Context = ExecutionContext ,
138
+ > ( ) : Promise < CloudflareContext < CfProperties , Context > > {
139
+ const cloudflareContext = getCloudflareContextFromGlobalScope < CfProperties , Context > ( ) ;
140
+
141
+ if ( cloudflareContext ) {
142
+ return cloudflareContext ;
143
+ }
144
+
145
+ // Note: Next.js sets process.env.NEXT_RUNTIME to 'nodejs' when the runtime in use is the node.js one
146
+ // We want to detect when the runtime is the node.js one so that during development (`next dev`) we know wether
147
+ // we are or not in a node.js process and that access to wrangler's node.js apis
148
+ const inNodejsRuntime = process . env . NEXT_RUNTIME === "nodejs" ;
149
+
150
+ if ( inNodejsRuntime || inSSG ( ) ) {
151
+ // we're in a node.js process and also in "async mode" so we can use wrangler to asynchronously get the context
152
+ const cloudflareContext = await getCloudflareContextFromWrangler < CfProperties , Context > ( ) ;
153
+ addCloudflareContextToNodejsGlobal ( cloudflareContext ) ;
154
+ return cloudflareContext ;
155
+ }
156
+
157
+ throw new Error ( initOpenNextCloudflareForDevErrorMsg ) ;
94
158
}
95
159
96
160
/**
@@ -127,12 +191,15 @@ function shouldContextInitializationRun(): boolean {
127
191
}
128
192
129
193
/**
130
- * Adds the cloudflare context to the global scope in which the Next.js dev node.js process runs in , enabling
194
+ * Adds the cloudflare context to the global scope of the current node.js process, enabling
131
195
* future calls to `getCloudflareContext` to retrieve and return such context
132
196
*
133
197
* @param cloudflareContext the cloudflare context to add to the node.sj global scope
134
198
*/
135
- function addCloudflareContextToNodejsGlobal ( cloudflareContext : CloudflareContext < CfProperties , Context > ) {
199
+ function addCloudflareContextToNodejsGlobal <
200
+ CfProperties extends Record < string , unknown > = IncomingRequestCfProperties ,
201
+ Context = ExecutionContext ,
202
+ > ( cloudflareContext : CloudflareContext < CfProperties , Context > ) {
136
203
const global = globalThis as InternalGlobalThis < CfProperties , Context > ;
137
204
global [ cloudflareContextSymbol ] = cloudflareContext ;
138
205
}
@@ -192,3 +259,18 @@ async function getCloudflareContextFromWrangler<
192
259
ctx : ctx as Context ,
193
260
} ;
194
261
}
262
+
263
+ // In production the cloudflare context is initialized by the worker so it is always available.
264
+ // During local development (`next dev`) it might be missing only if the developers hasn't called
265
+ // the `initOpenNextCloudflareForDev` function in their Next.js config file
266
+ const initOpenNextCloudflareForDevErrorMsg =
267
+ `\n\nERROR: \`getCloudflareContext\` has been called without having called` +
268
+ ` \`initOpenNextCloudflareForDev\` from the Next.js config file.\n` +
269
+ `You should update your Next.js config file as shown below:\n\n` +
270
+ " ```\n // next.config.mjs\n\n" +
271
+ ` import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";\n\n` +
272
+ ` initOpenNextCloudflareForDev();\n\n` +
273
+ " const nextConfig = { ... };\n" +
274
+ " export default nextConfig;\n" +
275
+ " ```\n" +
276
+ "\n" ;
0 commit comments