next_custom_transforms/transforms/
warn_for_edge_runtime.rs1use std::sync::Arc;
2
3use next_taskless::{EDGE_NODE_EXTERNALS, NODE_EXTERNALS};
4use swc_core::{
5 atoms::{Atom, Wtf8Atom},
6 common::{SourceMap, Span, errors::HANDLER},
7 ecma::{
8 ast::{
9 BinExpr, CallExpr, Callee, CondExpr, Expr, IdentName, IfStmt, ImportDecl, Lit,
10 MemberExpr, MemberProp, NamedExport, UnaryExpr, op,
11 },
12 utils::{ExprCtx, ExprExt},
13 visit::{Visit, VisitWith},
14 },
15};
16
17pub fn warn_for_edge_runtime(
18 cm: Arc<SourceMap>,
19 ctx: ExprCtx,
20 should_error_for_node_apis: bool,
21 is_production: bool,
22) -> impl Visit {
23 WarnForEdgeRuntime {
24 cm,
25 ctx,
26 should_error_for_node_apis,
27 should_add_guards: false,
28 guarded_symbols: Default::default(),
29 guarded_process_props: Default::default(),
30 guarded_runtime: false,
31 is_production,
32 emit_warn: |span: Span, msg: String| {
33 HANDLER.with(|h| {
34 h.struct_span_warn(span, &msg).emit();
35 });
36 },
37 emit_error: |span: Span, msg: String| {
38 HANDLER.with(|h| {
39 h.struct_span_err(span, &msg).emit();
40 });
41 },
42 }
43}
44
45pub fn warn_for_edge_runtime_with_handlers<EmitWarn, EmitError>(
46 cm: Arc<SourceMap>,
47 ctx: ExprCtx,
48 should_error_for_node_apis: bool,
49 is_production: bool,
50 emit_warn: EmitWarn,
51 emit_error: EmitError,
52) -> impl Visit
53where
54 EmitWarn: Fn(Span, String),
55 EmitError: Fn(Span, String),
56{
57 WarnForEdgeRuntime {
58 cm,
59 ctx,
60 should_error_for_node_apis,
61 should_add_guards: false,
62 guarded_symbols: Default::default(),
63 guarded_process_props: Default::default(),
64 guarded_runtime: false,
65 is_production,
66 emit_warn,
67 emit_error,
68 }
69}
70
71struct WarnForEdgeRuntime<EmitWarn, EmitError> {
82 cm: Arc<SourceMap>,
83 ctx: ExprCtx,
84 should_error_for_node_apis: bool,
85
86 should_add_guards: bool,
87 guarded_symbols: Vec<Atom>,
88 guarded_process_props: Vec<Atom>,
89 guarded_runtime: bool,
91 is_production: bool,
92 emit_warn: EmitWarn,
93 emit_error: EmitError,
94}
95
96const EDGE_UNSUPPORTED_NODE_APIS: &[&str] = &[
97 "clearImmediate",
98 "setImmediate",
99 "BroadcastChannel",
100 "ByteLengthQueuingStrategy",
101 "CompressionStream",
102 "CountQueuingStrategy",
103 "DecompressionStream",
104 "DomException",
105 "MessageChannel",
106 "MessageEvent",
107 "MessagePort",
108 "ReadableByteStreamController",
109 "ReadableStreamBYOBRequest",
110 "ReadableStreamDefaultController",
111 "TransformStreamDefaultController",
112 "WritableStreamDefaultController",
113];
114
115impl<EmitWarn, EmitError> WarnForEdgeRuntime<EmitWarn, EmitError>
116where
117 EmitWarn: Fn(Span, String),
118 EmitError: Fn(Span, String),
119{
120 fn warn_if_nodejs_module(&self, span: Span, module_specifier: &Wtf8Atom) -> Option<()> {
121 let module_specifier_str = module_specifier.as_str()?;
122 if self.guarded_runtime {
123 return None;
124 }
125
126 let module_name = module_specifier_str
127 .strip_prefix("node:")
128 .unwrap_or(module_specifier_str);
129
130 if NODE_EXTERNALS.binary_search(&module_name).is_ok()
131 && EDGE_NODE_EXTERNALS.binary_search(&module_name).is_err()
132 {
133 let loc = self.cm.lookup_line(span.lo).ok()?;
134
135 let msg = format!(
136 "A Node.js module is loaded ('{module_specifier_str}' at line {}) which is not \
137 supported in the Edge Runtime.
138Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime",
139 loc.line + 1
140 );
141
142 (self.emit_warn)(span, msg);
143 }
144
145 None
146 }
147
148 fn emit_unsupported_api_error(&self, span: Span, api_name: &str) -> Option<()> {
149 if self.guarded_runtime
150 || self
151 .guarded_symbols
152 .iter()
153 .any(|guarded| guarded == api_name)
154 {
155 return None;
156 }
157
158 let loc = self.cm.lookup_line(span.lo).ok()?;
159
160 let msg = format!(
161 "A Node.js API is used ({api_name} at line: {}) which is not supported in the Edge \
162 Runtime.
163Learn more: https://nextjs.org/docs/api-reference/edge-runtime",
164 loc.line + 1
165 );
166
167 if self.should_error_for_node_apis {
168 (self.emit_error)(span, msg);
169 } else {
170 (self.emit_warn)(span, msg);
171 }
172
173 None
174 }
175
176 fn is_in_middleware_layer(&self) -> bool {
177 true
178 }
179
180 fn warn_for_unsupported_process_api(&self, span: Span, prop: &IdentName) {
181 if !self.is_in_middleware_layer() || prop.sym == "env" {
182 return;
183 }
184 if self.guarded_runtime || self.guarded_process_props.contains(&prop.sym) {
185 return;
186 }
187
188 self.emit_unsupported_api_error(span, &format!("process.{}", prop.sym));
189 }
190
191 fn add_guards(&mut self, test: &Expr) {
192 let old = self.should_add_guards;
193 self.should_add_guards = true;
194 test.visit_with(self);
195 self.should_add_guards = old;
196 }
197
198 fn add_guard_for_test(&mut self, test: &Expr) {
199 if !self.should_add_guards {
200 return;
201 }
202
203 match test {
204 Expr::Ident(ident) => {
205 self.guarded_symbols.push(ident.sym.clone());
206 }
207 Expr::Member(member) => {
208 if member.prop.is_ident_with("NEXT_RUNTIME")
209 && let Expr::Member(obj_member) = &*member.obj
210 && obj_member.obj.is_global_ref_to(self.ctx, "process")
211 && obj_member.prop.is_ident_with("env")
212 {
213 self.guarded_runtime = true;
214 }
215 if member.obj.is_global_ref_to(self.ctx, "process")
216 && let MemberProp::Ident(prop) = &member.prop
217 {
218 self.guarded_process_props.push(prop.sym.clone());
219 }
220 }
221 Expr::Bin(BinExpr {
222 left,
223 right,
224 op: op!("===") | op!("==") | op!("!==") | op!("!="),
225 ..
226 }) => {
227 self.add_guard_for_test(left);
228 self.add_guard_for_test(right);
229 }
230 _ => (),
231 }
232 }
233
234 fn emit_dynamic_not_allowed_error(&self, span: Span) {
235 if self.is_production {
236 let msg = "Dynamic Code Evaluation (e. g. 'eval', 'new Function', \
237 'WebAssembly.compile') not allowed in Edge Runtime"
238 .to_string();
239
240 (self.emit_error)(span, msg);
241 }
242 }
243
244 fn with_new_scope(&mut self, f: impl FnOnce(&mut Self)) {
245 let old_guarded_symbols_len = self.guarded_symbols.len();
246 let old_guarded_process_props_len = self.guarded_symbols.len();
247 let old_guarded_runtime = self.guarded_runtime;
248 f(self);
249 self.guarded_symbols.truncate(old_guarded_symbols_len);
250 self.guarded_process_props
251 .truncate(old_guarded_process_props_len);
252 self.guarded_runtime = old_guarded_runtime;
253 }
254}
255
256impl<EmitWarn, EmitError> Visit for WarnForEdgeRuntime<EmitWarn, EmitError>
257where
258 EmitWarn: Fn(Span, String),
259 EmitError: Fn(Span, String),
260{
261 fn visit_call_expr(&mut self, n: &CallExpr) {
262 n.visit_children_with(self);
263
264 if let Callee::Import(_) = &n.callee
265 && let Some(Expr::Lit(Lit::Str(s))) = n.args.first().map(|e| &*e.expr)
266 {
267 self.warn_if_nodejs_module(n.span, &s.value);
268 }
269 }
270
271 fn visit_bin_expr(&mut self, node: &BinExpr) {
272 match node.op {
273 op!("&&") | op!("||") | op!("??") => {
274 if self.should_add_guards {
275 self.add_guards(&node.left);
277 node.right.visit_with(self);
278 } else {
279 self.with_new_scope(move |this| {
280 this.add_guards(&node.left);
281 node.right.visit_with(this);
282 });
283 }
284 }
285 op!("==") | op!("===") => {
286 self.add_guard_for_test(&node.left);
287 self.add_guard_for_test(&node.right);
288 node.visit_children_with(self);
289 }
290 _ => {
291 node.visit_children_with(self);
292 }
293 }
294 }
295 fn visit_cond_expr(&mut self, node: &CondExpr) {
296 self.with_new_scope(move |this| {
297 this.add_guards(&node.test);
298
299 node.cons.visit_with(this);
300 node.alt.visit_with(this);
301 });
302 }
303
304 fn visit_expr(&mut self, n: &Expr) {
305 if let Expr::Ident(ident) = n
306 && ident.ctxt == self.ctx.unresolved_ctxt
307 {
308 if ident.sym == "eval" {
309 self.emit_dynamic_not_allowed_error(ident.span);
310 return;
311 }
312
313 for api in EDGE_UNSUPPORTED_NODE_APIS {
314 if self.is_in_middleware_layer() && ident.sym == *api {
315 self.emit_unsupported_api_error(ident.span, api);
316 return;
317 }
318 }
319 }
320
321 n.visit_children_with(self);
322 }
323
324 fn visit_if_stmt(&mut self, node: &IfStmt) {
325 self.with_new_scope(move |this| {
326 this.add_guards(&node.test);
327
328 node.cons.visit_with(this);
329 node.alt.visit_with(this);
330 });
331 }
332
333 fn visit_import_decl(&mut self, n: &ImportDecl) {
334 n.visit_children_with(self);
335
336 self.warn_if_nodejs_module(n.span, &n.src.value);
337 }
338
339 fn visit_member_expr(&mut self, n: &MemberExpr) {
340 if n.obj.is_global_ref_to(self.ctx, "process")
341 && let MemberProp::Ident(prop) = &n.prop
342 {
343 self.warn_for_unsupported_process_api(n.span, prop);
344 return;
345 }
346
347 n.visit_children_with(self);
348 }
349
350 fn visit_named_export(&mut self, n: &NamedExport) {
351 n.visit_children_with(self);
352
353 if let Some(module_specifier) = &n.src {
354 self.warn_if_nodejs_module(n.span, &module_specifier.value);
355 }
356 }
357
358 fn visit_unary_expr(&mut self, node: &UnaryExpr) {
359 if node.op == op!("typeof") {
360 self.add_guard_for_test(&node.arg);
361 return;
362 }
363
364 node.visit_children_with(self);
365 }
366}