Skip to main content

next_custom_transforms/transforms/
warn_for_edge_runtime.rs

1use 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
71/// This is a very simple visitor that currently only checks if a condition (be it an if-statement
72/// or ternary expression) contains a reference to disallowed globals/etc.
73/// It does not know the difference between
74/// ```js
75/// if(typeof clearImmediate === "function") clearImmediate();
76/// ```
77/// and
78/// ```js
79/// if(typeof clearImmediate !== "function") clearImmediate();
80/// ```
81struct 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    // for process.env.NEXT_RUNTIME
90    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                    // This is a condition and not a shorthand for if-then
276                    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}