next_custom_transforms/transforms/
warn_for_edge_runtime.rs

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
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: &str) -> Option<()> {
191        if self.guarded_runtime {
192            return None;
193        }
194
195        // Node.js modules can be loaded with `node:` prefix or directly
196        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                    // This is a condition and not a shorthand for if-then
343                    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}