1use std::sync::Arc;
2
3use swc_core::{
4 atoms::Atom,
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: &str) -> Option<()> {
191 if self.guarded_runtime {
192 return None;
193 }
194
195 if module_specifier.starts_with("node:") || NODEJS_MODULE_NAMES.contains(&module_specifier)
197 {
198 let loc = self.cm.lookup_line(span.lo).ok()?;
199
200 let msg = format!(
201 "A Node.js module is loaded ('{module_specifier}' at line {}) which is not \
202 supported in the Edge Runtime.
203Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime",
204 loc.line + 1
205 );
206
207 (self.emit_warn)(span, msg);
208 }
209
210 None
211 }
212
213 fn emit_unsupported_api_error(&self, span: Span, api_name: &str) -> Option<()> {
214 if self.guarded_runtime
215 || self
216 .guarded_symbols
217 .iter()
218 .any(|guarded| guarded == api_name)
219 {
220 return None;
221 }
222
223 let loc = self.cm.lookup_line(span.lo).ok()?;
224
225 let msg = format!(
226 "A Node.js API is used ({api_name} at line: {}) which is not supported in the Edge \
227 Runtime.
228Learn more: https://nextjs.org/docs/api-reference/edge-runtime",
229 loc.line + 1
230 );
231
232 if self.should_error_for_node_apis {
233 (self.emit_error)(span, msg);
234 } else {
235 (self.emit_warn)(span, msg);
236 }
237
238 None
239 }
240
241 fn is_in_middleware_layer(&self) -> bool {
242 true
243 }
244
245 fn warn_for_unsupported_process_api(&self, span: Span, prop: &IdentName) {
246 if !self.is_in_middleware_layer() || prop.sym == "env" {
247 return;
248 }
249 if self.guarded_runtime || self.guarded_process_props.contains(&prop.sym) {
250 return;
251 }
252
253 self.emit_unsupported_api_error(span, &format!("process.{}", prop.sym));
254 }
255
256 fn add_guards(&mut self, test: &Expr) {
257 let old = self.should_add_guards;
258 self.should_add_guards = true;
259 test.visit_with(self);
260 self.should_add_guards = old;
261 }
262
263 fn add_guard_for_test(&mut self, test: &Expr) {
264 if !self.should_add_guards {
265 return;
266 }
267
268 match test {
269 Expr::Ident(ident) => {
270 self.guarded_symbols.push(ident.sym.clone());
271 }
272 Expr::Member(member) => {
273 if member.prop.is_ident_with("NEXT_RUNTIME") {
274 if let Expr::Member(obj_member) = &*member.obj {
275 if obj_member.obj.is_global_ref_to(self.ctx, "process")
276 && obj_member.prop.is_ident_with("env")
277 {
278 self.guarded_runtime = true;
279 }
280 }
281 }
282 if member.obj.is_global_ref_to(self.ctx, "process") {
283 if let MemberProp::Ident(prop) = &member.prop {
284 self.guarded_process_props.push(prop.sym.clone());
285 }
286 }
287 }
288 Expr::Bin(BinExpr {
289 left,
290 right,
291 op: op!("===") | op!("==") | op!("!==") | op!("!="),
292 ..
293 }) => {
294 self.add_guard_for_test(left);
295 self.add_guard_for_test(right);
296 }
297 _ => (),
298 }
299 }
300
301 fn emit_dynamic_not_allowed_error(&self, span: Span) {
302 if self.is_production {
303 let msg = "Dynamic Code Evaluation (e. g. 'eval', 'new Function', \
304 'WebAssembly.compile') not allowed in Edge Runtime"
305 .to_string();
306
307 (self.emit_error)(span, msg);
308 }
309 }
310
311 fn with_new_scope(&mut self, f: impl FnOnce(&mut Self)) {
312 let old_guarded_symbols_len = self.guarded_symbols.len();
313 let old_guarded_process_props_len = self.guarded_symbols.len();
314 let old_guarded_runtime = self.guarded_runtime;
315 f(self);
316 self.guarded_symbols.truncate(old_guarded_symbols_len);
317 self.guarded_process_props
318 .truncate(old_guarded_process_props_len);
319 self.guarded_runtime = old_guarded_runtime;
320 }
321}
322
323impl<EmitWarn, EmitError> Visit for WarnForEdgeRuntime<EmitWarn, EmitError>
324where
325 EmitWarn: Fn(Span, String),
326 EmitError: Fn(Span, String),
327{
328 fn visit_call_expr(&mut self, n: &CallExpr) {
329 n.visit_children_with(self);
330
331 if let Callee::Import(_) = &n.callee {
332 if let Some(Expr::Lit(Lit::Str(s))) = n.args.first().map(|e| &*e.expr) {
333 self.warn_if_nodejs_module(n.span, &s.value);
334 }
335 }
336 }
337
338 fn visit_bin_expr(&mut self, node: &BinExpr) {
339 match node.op {
340 op!("&&") | op!("||") | op!("??") => {
341 if self.should_add_guards {
342 self.add_guards(&node.left);
344 node.right.visit_with(self);
345 } else {
346 self.with_new_scope(move |this| {
347 this.add_guards(&node.left);
348 node.right.visit_with(this);
349 });
350 }
351 }
352 op!("==") | op!("===") => {
353 self.add_guard_for_test(&node.left);
354 self.add_guard_for_test(&node.right);
355 node.visit_children_with(self);
356 }
357 _ => {
358 node.visit_children_with(self);
359 }
360 }
361 }
362 fn visit_cond_expr(&mut self, node: &CondExpr) {
363 self.with_new_scope(move |this| {
364 this.add_guards(&node.test);
365
366 node.cons.visit_with(this);
367 node.alt.visit_with(this);
368 });
369 }
370
371 fn visit_expr(&mut self, n: &Expr) {
372 if let Expr::Ident(ident) = n {
373 if ident.ctxt == self.ctx.unresolved_ctxt {
374 if ident.sym == "eval" {
375 self.emit_dynamic_not_allowed_error(ident.span);
376 return;
377 }
378
379 for api in EDGE_UNSUPPORTED_NODE_APIS {
380 if self.is_in_middleware_layer() && ident.sym == *api {
381 self.emit_unsupported_api_error(ident.span, api);
382 return;
383 }
384 }
385 }
386 }
387
388 n.visit_children_with(self);
389 }
390
391 fn visit_if_stmt(&mut self, node: &IfStmt) {
392 self.with_new_scope(move |this| {
393 this.add_guards(&node.test);
394
395 node.cons.visit_with(this);
396 node.alt.visit_with(this);
397 });
398 }
399
400 fn visit_import_decl(&mut self, n: &ImportDecl) {
401 n.visit_children_with(self);
402
403 self.warn_if_nodejs_module(n.span, &n.src.value);
404 }
405
406 fn visit_member_expr(&mut self, n: &MemberExpr) {
407 if n.obj.is_global_ref_to(self.ctx, "process") {
408 if let MemberProp::Ident(prop) = &n.prop {
409 self.warn_for_unsupported_process_api(n.span, prop);
410 return;
411 }
412 }
413
414 n.visit_children_with(self);
415 }
416
417 fn visit_named_export(&mut self, n: &NamedExport) {
418 n.visit_children_with(self);
419
420 if let Some(module_specifier) = &n.src {
421 self.warn_if_nodejs_module(n.span, &module_specifier.value);
422 }
423 }
424
425 fn visit_unary_expr(&mut self, node: &UnaryExpr) {
426 if node.op == op!("typeof") {
427 self.add_guard_for_test(&node.arg);
428 return;
429 }
430
431 node.visit_children_with(self);
432 }
433}