next_custom_transforms/transforms/
warn_for_edge_runtime.rs

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
70/// This is a very simple visitor that currently only checks if a condition (be it an if-statement
71/// or ternary expression) contains a reference to disallowed globals/etc.
72/// It does not know the difference between
73/// ```js
74/// if(typeof clearImmediate === "function") clearImmediate();
75/// ```
76/// and
77/// ```js
78/// if(typeof clearImmediate !== "function") clearImmediate();
79/// ```
80struct 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    // for process.env.NEXT_RUNTIME
89    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
114/// https://vercel.com/docs/functions/runtimes/edge-runtime#compatible-node.js-modules
115const 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    // "assert",
131    // "assert/strict",
132    // "async_hooks",
133    // "buffer",
134    "child_process",
135    "cluster",
136    "console",
137    "constants",
138    "crypto",
139    "dgram",
140    "diagnostics_channel",
141    "dns",
142    "dns/promises",
143    "domain",
144    // "events",
145    "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    // "util",
177    // "util/types",
178    "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        // Node.js modules can be loaded with `node:` prefix or directly
197        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                    // This is a condition and not a shorthand for if-then
345                    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}