1use std::sync::Arc;
2
3use swc_core::{
4 atoms::{Atom, Wtf8Atom},
5 common::{errors::HANDLER, SourceMap, Span},
6 ecma::{
7 ast::{
8 op, BinExpr, CallExpr, Callee, CondExpr, Expr, IdentName, IfStmt, ImportDecl, Lit,
9 MemberExpr, MemberProp, NamedExport, UnaryExpr,
10 },
11 utils::{ExprCtx, ExprExt},
12 visit::{Visit, VisitWith},
13 },
14};
15
16pub fn warn_for_edge_runtime(
17 cm: Arc<SourceMap>,
18 ctx: ExprCtx,
19 should_error_for_node_apis: bool,
20 is_production: bool,
21) -> impl Visit {
22 WarnForEdgeRuntime {
23 cm,
24 ctx,
25 should_error_for_node_apis,
26 should_add_guards: false,
27 guarded_symbols: Default::default(),
28 guarded_process_props: Default::default(),
29 guarded_runtime: false,
30 is_production,
31 emit_warn: |span: Span, msg: String| {
32 HANDLER.with(|h| {
33 h.struct_span_warn(span, &msg).emit();
34 });
35 },
36 emit_error: |span: Span, msg: String| {
37 HANDLER.with(|h| {
38 h.struct_span_err(span, &msg).emit();
39 });
40 },
41 }
42}
43
44pub fn warn_for_edge_runtime_with_handlers<EmitWarn, EmitError>(
45 cm: Arc<SourceMap>,
46 ctx: ExprCtx,
47 should_error_for_node_apis: bool,
48 is_production: bool,
49 emit_warn: EmitWarn,
50 emit_error: EmitError,
51) -> impl Visit
52where
53 EmitWarn: Fn(Span, String),
54 EmitError: Fn(Span, String),
55{
56 WarnForEdgeRuntime {
57 cm,
58 ctx,
59 should_error_for_node_apis,
60 should_add_guards: false,
61 guarded_symbols: Default::default(),
62 guarded_process_props: Default::default(),
63 guarded_runtime: false,
64 is_production,
65 emit_warn,
66 emit_error,
67 }
68}
69
70struct WarnForEdgeRuntime<EmitWarn, EmitError> {
81 cm: Arc<SourceMap>,
82 ctx: ExprCtx,
83 should_error_for_node_apis: bool,
84
85 should_add_guards: bool,
86 guarded_symbols: Vec<Atom>,
87 guarded_process_props: Vec<Atom>,
88 guarded_runtime: bool,
90 is_production: bool,
91 emit_warn: EmitWarn,
92 emit_error: EmitError,
93}
94
95const EDGE_UNSUPPORTED_NODE_APIS: &[&str] = &[
96 "clearImmediate",
97 "setImmediate",
98 "BroadcastChannel",
99 "ByteLengthQueuingStrategy",
100 "CompressionStream",
101 "CountQueuingStrategy",
102 "DecompressionStream",
103 "DomException",
104 "MessageChannel",
105 "MessageEvent",
106 "MessagePort",
107 "ReadableByteStreamController",
108 "ReadableStreamBYOBRequest",
109 "ReadableStreamDefaultController",
110 "TransformStreamDefaultController",
111 "WritableStreamDefaultController",
112];
113
114const NODEJS_MODULE_NAMES: &[&str] = &[
116 "_http_agent",
117 "_http_client",
118 "_http_common",
119 "_http_incoming",
120 "_http_outgoing",
121 "_http_server",
122 "_stream_duplex",
123 "_stream_passthrough",
124 "_stream_readable",
125 "_stream_transform",
126 "_stream_wrap",
127 "_stream_writable",
128 "_tls_common",
129 "_tls_wrap",
130 "child_process",
135 "cluster",
136 "console",
137 "constants",
138 "crypto",
139 "dgram",
140 "diagnostics_channel",
141 "dns",
142 "dns/promises",
143 "domain",
144 "fs",
146 "fs/promises",
147 "http",
148 "http2",
149 "https",
150 "inspector",
151 "module",
152 "net",
153 "os",
154 "path",
155 "path/posix",
156 "path/win32",
157 "perf_hooks",
158 "process",
159 "punycode",
160 "querystring",
161 "readline",
162 "readline/promises",
163 "repl",
164 "stream",
165 "stream/consumers",
166 "stream/promises",
167 "stream/web",
168 "string_decoder",
169 "sys",
170 "timers",
171 "timers/promises",
172 "tls",
173 "trace_events",
174 "tty",
175 "url",
176 "v8",
179 "vm",
180 "wasi",
181 "worker_threads",
182 "zlib",
183];
184
185impl<EmitWarn, EmitError> WarnForEdgeRuntime<EmitWarn, EmitError>
186where
187 EmitWarn: Fn(Span, String),
188 EmitError: Fn(Span, String),
189{
190 fn warn_if_nodejs_module(&self, span: Span, module_specifier: &Wtf8Atom) -> Option<()> {
191 let module_specifier_str = module_specifier.as_str()?;
192 if self.guarded_runtime {
193 return None;
194 }
195
196 if module_specifier.starts_with("node:")
198 || NODEJS_MODULE_NAMES.contains(&module_specifier_str)
199 {
200 let loc = self.cm.lookup_line(span.lo).ok()?;
201
202 let msg = format!(
203 "A Node.js module is loaded ('{module_specifier_str}' at line {}) which is not \
204 supported in the Edge Runtime.
205Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime",
206 loc.line + 1
207 );
208
209 (self.emit_warn)(span, msg);
210 }
211
212 None
213 }
214
215 fn emit_unsupported_api_error(&self, span: Span, api_name: &str) -> Option<()> {
216 if self.guarded_runtime
217 || self
218 .guarded_symbols
219 .iter()
220 .any(|guarded| guarded == api_name)
221 {
222 return None;
223 }
224
225 let loc = self.cm.lookup_line(span.lo).ok()?;
226
227 let msg = format!(
228 "A Node.js API is used ({api_name} at line: {}) which is not supported in the Edge \
229 Runtime.
230Learn more: https://nextjs.org/docs/api-reference/edge-runtime",
231 loc.line + 1
232 );
233
234 if self.should_error_for_node_apis {
235 (self.emit_error)(span, msg);
236 } else {
237 (self.emit_warn)(span, msg);
238 }
239
240 None
241 }
242
243 fn is_in_middleware_layer(&self) -> bool {
244 true
245 }
246
247 fn warn_for_unsupported_process_api(&self, span: Span, prop: &IdentName) {
248 if !self.is_in_middleware_layer() || prop.sym == "env" {
249 return;
250 }
251 if self.guarded_runtime || self.guarded_process_props.contains(&prop.sym) {
252 return;
253 }
254
255 self.emit_unsupported_api_error(span, &format!("process.{}", prop.sym));
256 }
257
258 fn add_guards(&mut self, test: &Expr) {
259 let old = self.should_add_guards;
260 self.should_add_guards = true;
261 test.visit_with(self);
262 self.should_add_guards = old;
263 }
264
265 fn add_guard_for_test(&mut self, test: &Expr) {
266 if !self.should_add_guards {
267 return;
268 }
269
270 match test {
271 Expr::Ident(ident) => {
272 self.guarded_symbols.push(ident.sym.clone());
273 }
274 Expr::Member(member) => {
275 if member.prop.is_ident_with("NEXT_RUNTIME") {
276 if let Expr::Member(obj_member) = &*member.obj {
277 if obj_member.obj.is_global_ref_to(self.ctx, "process")
278 && obj_member.prop.is_ident_with("env")
279 {
280 self.guarded_runtime = true;
281 }
282 }
283 }
284 if member.obj.is_global_ref_to(self.ctx, "process") {
285 if let MemberProp::Ident(prop) = &member.prop {
286 self.guarded_process_props.push(prop.sym.clone());
287 }
288 }
289 }
290 Expr::Bin(BinExpr {
291 left,
292 right,
293 op: op!("===") | op!("==") | op!("!==") | op!("!="),
294 ..
295 }) => {
296 self.add_guard_for_test(left);
297 self.add_guard_for_test(right);
298 }
299 _ => (),
300 }
301 }
302
303 fn emit_dynamic_not_allowed_error(&self, span: Span) {
304 if self.is_production {
305 let msg = "Dynamic Code Evaluation (e. g. 'eval', 'new Function', \
306 'WebAssembly.compile') not allowed in Edge Runtime"
307 .to_string();
308
309 (self.emit_error)(span, msg);
310 }
311 }
312
313 fn with_new_scope(&mut self, f: impl FnOnce(&mut Self)) {
314 let old_guarded_symbols_len = self.guarded_symbols.len();
315 let old_guarded_process_props_len = self.guarded_symbols.len();
316 let old_guarded_runtime = self.guarded_runtime;
317 f(self);
318 self.guarded_symbols.truncate(old_guarded_symbols_len);
319 self.guarded_process_props
320 .truncate(old_guarded_process_props_len);
321 self.guarded_runtime = old_guarded_runtime;
322 }
323}
324
325impl<EmitWarn, EmitError> Visit for WarnForEdgeRuntime<EmitWarn, EmitError>
326where
327 EmitWarn: Fn(Span, String),
328 EmitError: Fn(Span, String),
329{
330 fn visit_call_expr(&mut self, n: &CallExpr) {
331 n.visit_children_with(self);
332
333 if let Callee::Import(_) = &n.callee {
334 if let Some(Expr::Lit(Lit::Str(s))) = n.args.first().map(|e| &*e.expr) {
335 self.warn_if_nodejs_module(n.span, &s.value);
336 }
337 }
338 }
339
340 fn visit_bin_expr(&mut self, node: &BinExpr) {
341 match node.op {
342 op!("&&") | op!("||") | op!("??") => {
343 if self.should_add_guards {
344 self.add_guards(&node.left);
346 node.right.visit_with(self);
347 } else {
348 self.with_new_scope(move |this| {
349 this.add_guards(&node.left);
350 node.right.visit_with(this);
351 });
352 }
353 }
354 op!("==") | op!("===") => {
355 self.add_guard_for_test(&node.left);
356 self.add_guard_for_test(&node.right);
357 node.visit_children_with(self);
358 }
359 _ => {
360 node.visit_children_with(self);
361 }
362 }
363 }
364 fn visit_cond_expr(&mut self, node: &CondExpr) {
365 self.with_new_scope(move |this| {
366 this.add_guards(&node.test);
367
368 node.cons.visit_with(this);
369 node.alt.visit_with(this);
370 });
371 }
372
373 fn visit_expr(&mut self, n: &Expr) {
374 if let Expr::Ident(ident) = n {
375 if ident.ctxt == self.ctx.unresolved_ctxt {
376 if ident.sym == "eval" {
377 self.emit_dynamic_not_allowed_error(ident.span);
378 return;
379 }
380
381 for api in EDGE_UNSUPPORTED_NODE_APIS {
382 if self.is_in_middleware_layer() && ident.sym == *api {
383 self.emit_unsupported_api_error(ident.span, api);
384 return;
385 }
386 }
387 }
388 }
389
390 n.visit_children_with(self);
391 }
392
393 fn visit_if_stmt(&mut self, node: &IfStmt) {
394 self.with_new_scope(move |this| {
395 this.add_guards(&node.test);
396
397 node.cons.visit_with(this);
398 node.alt.visit_with(this);
399 });
400 }
401
402 fn visit_import_decl(&mut self, n: &ImportDecl) {
403 n.visit_children_with(self);
404
405 self.warn_if_nodejs_module(n.span, &n.src.value);
406 }
407
408 fn visit_member_expr(&mut self, n: &MemberExpr) {
409 if n.obj.is_global_ref_to(self.ctx, "process") {
410 if let MemberProp::Ident(prop) = &n.prop {
411 self.warn_for_unsupported_process_api(n.span, prop);
412 return;
413 }
414 }
415
416 n.visit_children_with(self);
417 }
418
419 fn visit_named_export(&mut self, n: &NamedExport) {
420 n.visit_children_with(self);
421
422 if let Some(module_specifier) = &n.src {
423 self.warn_if_nodejs_module(n.span, &module_specifier.value);
424 }
425 }
426
427 fn visit_unary_expr(&mut self, node: &UnaryExpr) {
428 if node.op == op!("typeof") {
429 self.add_guard_for_test(&node.arg);
430 return;
431 }
432
433 node.visit_children_with(self);
434 }
435}