Skip to main content

next_custom_transforms/transforms/
strip_page_exports.rs

1//! The original transform is available on the [Next.js repository](https://github.com/vercel/next.js/blob/f7fecf00cb40c2f784387ff8ccc5e213b8bdd9ca/packages/next-swc/crates/core/src/next_ssg.rs):
2//!
3//! This version adds support for eliminating client-side exports only.
4//! **TODO** may consolidate into next_ssg
5
6use std::{cell::RefCell, mem::take, rc::Rc};
7
8use rustc_hash::{FxHashMap, FxHashSet};
9use swc_core::{
10    atoms::Atom,
11    common::{
12        DUMMY_SP,
13        errors::HANDLER,
14        pass::{Repeat, Repeated},
15    },
16    ecma::{
17        ast::*,
18        visit::{Fold, FoldWith, Visit, VisitWith, fold_pass, noop_fold_type, noop_visit_type},
19    },
20};
21
22/// Determines which exports to remove.
23#[derive(Debug, Default, Clone, Copy)]
24pub enum ExportFilter {
25    /// Strip all data exports (getServerSideProps,
26    /// getStaticProps, getStaticPaths exports.) and their unique dependencies.
27    #[default]
28    StripDataExports,
29    /// Strip default export and all its unique dependencies.
30    StripDefaultExport,
31}
32
33#[derive(Debug, Default, Clone, Copy)]
34pub enum PageMode {
35    #[default]
36    None,
37    /// The Next.js page is declaring `getServerSideProps`.
38    Ssr,
39    /// The Next.js page is declaring `getStaticProps` and/or `getStaticPaths`.
40    Ssg,
41}
42
43impl PageMode {
44    /// Which identifier (if any) to export in the output file.
45    fn data_marker(self) -> Option<&'static str> {
46        match self {
47            PageMode::None => None,
48            PageMode::Ssr => Some("__N_SSP"),
49            PageMode::Ssg => Some("__N_SSG"),
50        }
51    }
52}
53
54/// A transform that either:
55/// * strips Next.js data exports (getServerSideProps, getStaticProps, getStaticPaths); or
56/// * strips the default export.
57///
58/// Note: This transform requires running `resolver` **before** running it.
59pub fn next_transform_strip_page_exports(
60    filter: ExportFilter,
61    ssr_removed_packages: Rc<RefCell<FxHashSet<Atom>>>,
62) -> impl Pass {
63    fold_pass(Repeat::new(NextSsg {
64        state: State {
65            ssr_removed_packages,
66            filter,
67            ..Default::default()
68        },
69        in_lhs_of_var: false,
70        remove_expression: false,
71    }))
72}
73
74/// State of the transforms. Shared by the analyzer and the transform.
75#[derive(Debug, Default)]
76struct State {
77    filter: ExportFilter,
78
79    page_mode: PageMode,
80
81    exports: FxHashMap<Id, ExportType>,
82
83    /// Identifiers referenced in the body of preserved functions.
84    ///
85    /// Cleared before running each pass, because we drop ast nodes between the
86    /// passes.
87    refs_from_preserved: FxHashSet<Id>,
88
89    /// Identifiers referenced in the body of removed functions or
90    /// derivatives.
91    ///
92    /// Preserved between runs, because we should remember derivatives of data
93    /// functions as the data function itself is already removed.
94    refs_from_removed: FxHashSet<Id>,
95
96    /// Identifiers of functions currently being declared, the body of which we
97    /// are currently visiting.
98    cur_declaring: FxHashSet<Id>,
99
100    /// `true` if the transform has added a page mode marker to the AST.
101    added_data_marker: bool,
102
103    should_run_again: bool,
104
105    /// Track the import packages which are removed alongside
106    /// `getServerSideProps` in SSR.
107    ssr_removed_packages: Rc<RefCell<FxHashSet<Atom>>>,
108}
109
110/// The type of export associated to an identifier.
111#[derive(Debug, Clone, Copy)]
112enum ExportType {
113    Default,
114    GetServerSideProps,
115    GetStaticPaths,
116    GetStaticProps,
117}
118
119impl ExportType {
120    fn from_specifier(specifier: &ExportSpecifier) -> Option<ExportTypeResult<'_>> {
121        match specifier {
122            ExportSpecifier::Default(ExportDefaultSpecifier { exported, .. })
123            | ExportSpecifier::Namespace(ExportNamespaceSpecifier {
124                name: ModuleExportName::Ident(exported),
125                ..
126            }) => {
127                let export_type = ExportType::from_ident(exported)?;
128                Some(ExportTypeResult {
129                    exported_ident: exported,
130                    local_ident: None,
131                    export_type,
132                })
133            }
134
135            ExportSpecifier::Named(ExportNamedSpecifier {
136                exported: Some(ModuleExportName::Ident(exported)),
137                orig: ModuleExportName::Ident(orig),
138                ..
139            })
140            | ExportSpecifier::Named(ExportNamedSpecifier {
141                orig: ModuleExportName::Ident(orig @ exported),
142                ..
143            }) => {
144                let export_type = ExportType::from_ident(exported)?;
145                Some(ExportTypeResult {
146                    exported_ident: exported,
147                    local_ident: Some(orig),
148                    export_type,
149                })
150            }
151            _ => None,
152        }
153    }
154
155    fn from_ident(ident: &Ident) -> Option<Self> {
156        Some(match &*ident.sym {
157            "default" => ExportType::Default,
158            "getStaticProps" => ExportType::GetStaticProps,
159            "getStaticPaths" => ExportType::GetStaticPaths,
160            "getServerSideProps" => ExportType::GetServerSideProps,
161            _ => return None,
162        })
163    }
164}
165
166struct ExportTypeResult<'a> {
167    exported_ident: &'a Ident,
168    local_ident: Option<&'a Ident>,
169    export_type: ExportType,
170}
171
172impl State {
173    fn encounter_export(
174        &mut self,
175        exported_ident: &Ident,
176        local_ident: Option<&Ident>,
177        export_type: ExportType,
178    ) {
179        match export_type {
180            ExportType::GetServerSideProps => {
181                if matches!(self.page_mode, PageMode::Ssg) {
182                    HANDLER.with(|handler| {
183                        handler
184                            .struct_span_err(
185                                exported_ident.span,
186                                "You can not use getStaticProps or getStaticPaths with \
187                                 getServerSideProps. To use SSG, please remove getServerSideProps",
188                            )
189                            .emit()
190                    });
191                    return;
192                }
193
194                self.page_mode = PageMode::Ssr;
195            }
196            ExportType::GetStaticPaths | ExportType::GetStaticProps => {
197                if matches!(self.page_mode, PageMode::Ssr) {
198                    HANDLER.with(|handler| {
199                        handler
200                            .struct_span_err(
201                                exported_ident.span,
202                                "You can not use getStaticProps or getStaticPaths with \
203                                 getServerSideProps. To use SSG, please remove getServerSideProps",
204                            )
205                            .emit()
206                    });
207                    return;
208                }
209
210                self.page_mode = PageMode::Ssg;
211            }
212            _ => {}
213        }
214
215        let local_ident = local_ident.unwrap_or(exported_ident);
216
217        self.exports.insert(local_ident.to_id(), export_type);
218    }
219
220    fn export_type(&self, id: &Id) -> Option<ExportType> {
221        self.exports.get(id).copied()
222    }
223
224    fn should_retain_export_type(&self, export_type: ExportType) -> bool {
225        !matches!(
226            (self.filter, export_type),
227            (
228                ExportFilter::StripDataExports,
229                ExportType::GetServerSideProps
230                    | ExportType::GetStaticProps
231                    | ExportType::GetStaticPaths,
232            ) | (ExportFilter::StripDefaultExport, ExportType::Default)
233        )
234    }
235
236    fn should_retain_id(&self, id: &Id) -> bool {
237        if let Some(export_type) = self.export_type(id) {
238            self.should_retain_export_type(export_type)
239        } else {
240            true
241        }
242    }
243
244    fn dropping_export(&mut self, export_type: ExportType) -> bool {
245        if !self.should_retain_export_type(export_type) {
246            // If there are any assignments on the exported identifier, they'll
247            // need to be removed as well in the next pass.
248            self.should_run_again = true;
249            true
250        } else {
251            false
252        }
253    }
254}
255
256struct Analyzer<'a> {
257    state: &'a mut State,
258    in_lhs_of_var: bool,
259    in_removed_item: bool,
260}
261
262impl Analyzer<'_> {
263    fn add_ref(&mut self, id: Id) {
264        tracing::trace!(
265            "add_ref({}{:?}, in_removed_item = {:?})",
266            id.0,
267            id.1,
268            self.in_removed_item,
269        );
270        if self.in_removed_item {
271            self.state.refs_from_removed.insert(id);
272        } else {
273            if self.state.cur_declaring.contains(&id) {
274                return;
275            }
276
277            self.state.refs_from_preserved.insert(id);
278        }
279    }
280
281    fn within_declaration<R>(&mut self, id: &Id, f: impl FnOnce(&mut Self) -> R) -> R {
282        self.state.cur_declaring.insert(id.clone());
283        let res = f(self);
284        self.state.cur_declaring.remove(id);
285        res
286    }
287
288    fn within_removed_item<R>(
289        &mut self,
290        in_removed_item: bool,
291        f: impl FnOnce(&mut Self) -> R,
292    ) -> R {
293        let old = self.in_removed_item;
294        // `in_removed_item` is strictly additive.
295        self.in_removed_item |= in_removed_item;
296        let res = f(self);
297        self.in_removed_item = old;
298        res
299    }
300
301    fn within_lhs_of_var<R>(&mut self, in_lhs_of_var: bool, f: impl FnOnce(&mut Self) -> R) -> R {
302        let old = self.in_lhs_of_var;
303        self.in_lhs_of_var = in_lhs_of_var;
304        let res = f(self);
305        self.in_lhs_of_var = old;
306        res
307    }
308
309    fn visit_declaration<D>(&mut self, id: &Id, d: &D)
310    where
311        D: VisitWith<Self>,
312    {
313        self.within_declaration(id, |this| {
314            let in_removed_item = !this.state.should_retain_id(id);
315            this.within_removed_item(in_removed_item, |this| {
316                tracing::trace!(
317                    "transform_page: Handling `{}{:?}`; in_removed_item = {:?}",
318                    id.0,
319                    id.1,
320                    this.in_removed_item
321                );
322
323                d.visit_children_with(this);
324            });
325        });
326    }
327}
328
329impl Visit for Analyzer<'_> {
330    // This is important for reducing binary sizes.
331    noop_visit_type!();
332
333    fn visit_binding_ident(&mut self, i: &BindingIdent) {
334        if !self.in_lhs_of_var || self.in_removed_item {
335            self.add_ref(i.id.to_id());
336        }
337    }
338
339    fn visit_named_export(&mut self, n: &NamedExport) {
340        for specifier in &n.specifiers {
341            if let Some(ExportTypeResult {
342                exported_ident,
343                local_ident,
344                export_type,
345            }) = ExportType::from_specifier(specifier)
346            {
347                self.state
348                    .encounter_export(exported_ident, local_ident, export_type);
349
350                if let Some(local_ident) = local_ident
351                    && self.state.should_retain_export_type(export_type)
352                {
353                    self.add_ref(local_ident.to_id());
354                }
355            }
356        }
357    }
358
359    fn visit_export_decl(&mut self, s: &ExportDecl) {
360        match &s.decl {
361            Decl::Var(d) => {
362                for decl in &d.decls {
363                    if let Pat::Ident(ident) = &decl.name {
364                        if let Some(export_type) = ExportType::from_ident(ident) {
365                            self.state.encounter_export(ident, None, export_type);
366
367                            let retain = self.state.should_retain_export_type(export_type);
368
369                            if retain {
370                                self.add_ref(ident.to_id());
371                            }
372
373                            self.within_removed_item(!retain, |this| {
374                                decl.visit_with(this);
375                            });
376                        } else {
377                            // Always preserve declarations of unknown exports.
378                            self.add_ref(ident.to_id());
379
380                            decl.visit_with(self)
381                        }
382                    } else {
383                        decl.visit_with(self)
384                    }
385                }
386            }
387            Decl::Fn(decl) => {
388                let ident = &decl.ident;
389                if let Some(export_type) = ExportType::from_ident(ident) {
390                    self.state.encounter_export(ident, None, export_type);
391
392                    let retain = self.state.should_retain_export_type(export_type);
393
394                    if retain {
395                        self.add_ref(ident.to_id());
396                    }
397
398                    self.within_removed_item(!retain, |this| {
399                        decl.visit_with(this);
400                    });
401                } else {
402                    s.visit_children_with(self);
403                }
404            }
405            _ => s.visit_children_with(self),
406        }
407    }
408
409    fn visit_export_default_decl(&mut self, s: &ExportDefaultDecl) {
410        match &s.decl {
411            DefaultDecl::Class(ClassExpr {
412                ident: Some(ident), ..
413            }) => self
414                .state
415                .encounter_export(ident, Some(ident), ExportType::Default),
416            DefaultDecl::Fn(FnExpr {
417                ident: Some(ident), ..
418            }) => self
419                .state
420                .encounter_export(ident, Some(ident), ExportType::Default),
421            _ => {}
422        }
423        self.within_removed_item(
424            matches!(self.state.filter, ExportFilter::StripDefaultExport),
425            |this| {
426                s.visit_children_with(this);
427            },
428        );
429    }
430
431    fn visit_export_default_expr(&mut self, s: &ExportDefaultExpr) {
432        self.within_removed_item(
433            matches!(self.state.filter, ExportFilter::StripDefaultExport),
434            |this| {
435                s.visit_children_with(this);
436            },
437        );
438    }
439
440    fn visit_expr(&mut self, e: &Expr) {
441        e.visit_children_with(self);
442
443        if let Expr::Ident(i) = &e {
444            self.add_ref(i.to_id());
445        }
446    }
447
448    fn visit_jsx_element(&mut self, jsx: &JSXElement) {
449        fn get_leftmost_id_member_expr(e: &JSXMemberExpr) -> Id {
450            match &e.obj {
451                JSXObject::Ident(i) => i.to_id(),
452                JSXObject::JSXMemberExpr(e) => get_leftmost_id_member_expr(e),
453            }
454        }
455
456        match &jsx.opening.name {
457            JSXElementName::Ident(i) => {
458                self.add_ref(i.to_id());
459            }
460            JSXElementName::JSXMemberExpr(e) => {
461                self.add_ref(get_leftmost_id_member_expr(e));
462            }
463            _ => {}
464        }
465
466        jsx.visit_children_with(self);
467    }
468
469    fn visit_fn_decl(&mut self, f: &FnDecl) {
470        self.visit_declaration(&f.ident.to_id(), f);
471    }
472
473    fn visit_class_decl(&mut self, c: &ClassDecl) {
474        self.visit_declaration(&c.ident.to_id(), c);
475    }
476
477    fn visit_fn_expr(&mut self, f: &FnExpr) {
478        f.visit_children_with(self);
479
480        if let Some(id) = &f.ident {
481            self.add_ref(id.to_id());
482        }
483    }
484
485    fn visit_prop(&mut self, p: &Prop) {
486        p.visit_children_with(self);
487
488        if let Prop::Shorthand(i) = &p {
489            self.add_ref(i.to_id());
490        }
491    }
492
493    fn visit_var_declarator(&mut self, v: &VarDeclarator) {
494        let in_removed_item = if let Pat::Ident(name) = &v.name {
495            !self.state.should_retain_id(&name.id.to_id())
496        } else {
497            false
498        };
499
500        self.within_removed_item(in_removed_item, |this| {
501            this.within_lhs_of_var(true, |this| {
502                v.name.visit_with(this);
503            });
504
505            this.within_lhs_of_var(false, |this| {
506                v.init.visit_with(this);
507            });
508        });
509    }
510
511    fn visit_member_expr(&mut self, e: &MemberExpr) {
512        let in_removed_item = if let Some(id) = find_member_root_id(e) {
513            !self.state.should_retain_id(&id)
514        } else {
515            false
516        };
517
518        self.within_removed_item(in_removed_item, |this| {
519            e.visit_children_with(this);
520        });
521    }
522
523    fn visit_assign_expr(&mut self, e: &AssignExpr) {
524        self.within_lhs_of_var(true, |this| {
525            e.left.visit_with(this);
526        });
527
528        self.within_lhs_of_var(false, |this| {
529            e.right.visit_with(this);
530        });
531    }
532}
533
534/// Actual implementation of the transform.
535struct NextSsg {
536    pub state: State,
537    in_lhs_of_var: bool,
538    /// Marker set when a top-level expression item should be removed. This
539    /// occurs when visiting assignments on eliminated identifiers.
540    remove_expression: bool,
541}
542
543impl NextSsg {
544    /// Returns `true` when an identifier should be removed from the output.
545    fn should_remove(&self, id: &Id) -> bool {
546        self.state.refs_from_removed.contains(id) && !self.state.refs_from_preserved.contains(id)
547    }
548
549    /// Mark identifiers in `n` as a candidate for elimination.
550    fn mark_as_candidate<N>(&mut self, n: &N)
551    where
552        N: for<'aa> VisitWith<Analyzer<'aa>> + std::fmt::Debug,
553    {
554        tracing::debug!("mark_as_candidate: {:?}", n);
555
556        let mut v = Analyzer {
557            state: &mut self.state,
558            in_lhs_of_var: false,
559            // Analyzer never change `in_removed_item`, so all identifiers in `n`
560            // will be marked as referenced from an removed item.
561            in_removed_item: true,
562        };
563
564        n.visit_with(&mut v);
565        self.state.should_run_again = true;
566    }
567
568    /// Adds __N_SSG and __N_SSP declarations when eliminating data functions.
569    fn maybe_add_data_marker(&mut self, items: &mut Vec<ModuleItem>) {
570        if !matches!(self.state.filter, ExportFilter::StripDataExports)
571            || self.state.added_data_marker
572            || self.state.should_run_again
573        {
574            return;
575        }
576
577        let Some(data_marker) = self.state.page_mode.data_marker() else {
578            return;
579        };
580
581        self.state.added_data_marker = true;
582
583        if items.iter().any(|s| s.is_module_decl()) {
584            let insert_idx = items.iter().position(|item| {
585                matches!(
586                    item,
587                    ModuleItem::ModuleDecl(
588                        ModuleDecl::ExportNamed(..)
589                            | ModuleDecl::ExportDecl(..)
590                            | ModuleDecl::ExportDefaultDecl(..)
591                            | ModuleDecl::ExportDefaultExpr(..),
592                    )
593                )
594            });
595
596            if let Some(insert_idx) = insert_idx {
597                items.insert(
598                    insert_idx,
599                    ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
600                        span: DUMMY_SP,
601                        decl: Decl::Var(Box::new(VarDecl {
602                            span: DUMMY_SP,
603                            kind: VarDeclKind::Var,
604                            decls: vec![VarDeclarator {
605                                span: DUMMY_SP,
606                                name: Pat::Ident(
607                                    IdentName::new(data_marker.into(), DUMMY_SP).into(),
608                                ),
609                                init: Some(true.into()),
610                                definite: Default::default(),
611                            }],
612                            ..Default::default()
613                        })),
614                    })),
615                );
616            }
617        }
618    }
619
620    fn within_lhs_of_var<R>(&mut self, in_lhs_of_var: bool, f: impl FnOnce(&mut Self) -> R) -> R {
621        let old = self.in_lhs_of_var;
622        self.in_lhs_of_var = in_lhs_of_var;
623        let res = f(self);
624        self.in_lhs_of_var = old;
625        res
626    }
627}
628
629impl Repeated for NextSsg {
630    fn changed(&self) -> bool {
631        self.state.should_run_again
632    }
633
634    fn reset(&mut self) {
635        self.state.refs_from_preserved.clear();
636        self.state.cur_declaring.clear();
637        self.state.should_run_again = false;
638    }
639}
640
641/// `VisitMut` is faster than [Fold], but we use [Fold] because it's much easier
642/// to read.
643///
644/// Note: We don't implement `fold_script` because next.js doesn't use it.
645impl Fold for NextSsg {
646    fn fold_array_pat(&mut self, mut arr: ArrayPat) -> ArrayPat {
647        arr = arr.fold_children_with(self);
648
649        if !arr.elems.is_empty() {
650            arr.elems.retain(|e| !matches!(e, Some(Pat::Invalid(..))));
651        }
652
653        arr
654    }
655
656    fn fold_assign_target_pat(&mut self, mut n: AssignTargetPat) -> AssignTargetPat {
657        n = n.fold_children_with(self);
658
659        match &n {
660            AssignTargetPat::Array(arr) if arr.elems.is_empty() => {
661                return AssignTargetPat::Invalid(Invalid { span: DUMMY_SP });
662            }
663            AssignTargetPat::Object(obj) if obj.props.is_empty() => {
664                return AssignTargetPat::Invalid(Invalid { span: DUMMY_SP });
665            }
666            _ => {}
667        }
668
669        n
670    }
671
672    fn fold_expr(&mut self, e: Expr) -> Expr {
673        match e {
674            Expr::Assign(assign_expr) => {
675                let mut retain = true;
676                let left =
677                    self.within_lhs_of_var(true, |this| assign_expr.left.clone().fold_with(this));
678
679                let right = self.within_lhs_of_var(false, |this| {
680                    match left {
681                        AssignTarget::Simple(SimpleAssignTarget::Invalid(..))
682                        | AssignTarget::Pat(AssignTargetPat::Invalid(..)) => {
683                            retain = false;
684                            this.mark_as_candidate(&assign_expr.right);
685                        }
686
687                        _ => {}
688                    }
689                    assign_expr.right.clone().fold_with(this)
690                });
691
692                if retain {
693                    self.remove_expression = false;
694                    Expr::Assign(AssignExpr {
695                        left,
696                        right,
697                        ..assign_expr
698                    })
699                } else {
700                    self.remove_expression = true;
701                    *right
702                }
703            }
704            _ => {
705                self.remove_expression = false;
706                e.fold_children_with(self)
707            }
708        }
709    }
710
711    fn fold_import_decl(&mut self, mut i: ImportDecl) -> ImportDecl {
712        // Imports for side effects.
713        if i.specifiers.is_empty() {
714            return i;
715        }
716
717        let import_src = &i.src.value;
718
719        i.specifiers.retain(|s| match s {
720            ImportSpecifier::Named(ImportNamedSpecifier { local, .. })
721            | ImportSpecifier::Default(ImportDefaultSpecifier { local, .. })
722            | ImportSpecifier::Namespace(ImportStarAsSpecifier { local, .. }) => {
723                if self.should_remove(&local.to_id()) {
724                    if matches!(self.state.page_mode, PageMode::Ssr)
725                        && matches!(self.state.filter, ExportFilter::StripDataExports)
726                        // filter out non-packages import
727                        // third part packages must start with `a-z` or `@`
728                        && import_src.as_str().unwrap_or_default().starts_with(|c: char| c.is_ascii_lowercase() || c == '@')
729                    {
730                        self.state
731                            .ssr_removed_packages
732                            .borrow_mut()
733                            .insert(import_src.clone().to_atom_lossy().into_owned());
734                    }
735                    tracing::trace!(
736                        "Dropping import `{}{:?}` because it should be removed",
737                        local.sym,
738                        local.ctxt
739                    );
740
741                    self.state.should_run_again = true;
742                    false
743                } else {
744                    true
745                }
746            }
747        });
748
749        i
750    }
751
752    fn fold_module(&mut self, m: Module) -> Module {
753        tracing::info!("ssg: Start");
754        {
755            // Fill the state.
756            let mut v = Analyzer {
757                state: &mut self.state,
758                in_lhs_of_var: false,
759                in_removed_item: false,
760            };
761            m.visit_with(&mut v);
762        }
763
764        // TODO: Use better detection logic
765        // if let PageMode::None = self.state.page_mode {
766        //     return m;
767        // }
768
769        m.fold_children_with(self)
770    }
771
772    fn fold_module_item(&mut self, i: ModuleItem) -> ModuleItem {
773        if let ModuleItem::ModuleDecl(ModuleDecl::Import(i)) = i {
774            let is_for_side_effect = i.specifiers.is_empty();
775            let i = i.fold_with(self);
776
777            if !is_for_side_effect && i.specifiers.is_empty() {
778                return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }));
779            }
780
781            return ModuleItem::ModuleDecl(ModuleDecl::Import(i));
782        }
783
784        let i = i.fold_children_with(self);
785
786        match &i {
787            ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)) if e.specifiers.is_empty() => {
788                return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }));
789            }
790            ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(e)) => match &e.decl {
791                Decl::Fn(f) => {
792                    if let Some(export_type) = self.state.export_type(&f.ident.to_id())
793                        && self.state.dropping_export(export_type)
794                    {
795                        tracing::trace!(
796                            "Dropping an export specifier because it's an SSR/SSG function"
797                        );
798                        return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }));
799                    }
800                }
801
802                Decl::Var(d) if d.decls.is_empty() => {
803                    return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }));
804                }
805                _ => {}
806            },
807
808            ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(_))
809            | ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(_))
810                if self.state.dropping_export(ExportType::Default) =>
811            {
812                tracing::trace!("Dropping an export specifier because it's a default export");
813
814                return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }));
815            }
816            _ => {}
817        }
818
819        i
820    }
821
822    fn fold_module_items(&mut self, mut items: Vec<ModuleItem>) -> Vec<ModuleItem> {
823        items = items.fold_children_with(self);
824
825        // Drop empty nodes.
826        items.retain(|s| !matches!(s, ModuleItem::Stmt(Stmt::Empty(..))));
827
828        self.maybe_add_data_marker(&mut items);
829
830        items
831    }
832
833    fn fold_named_export(&mut self, mut n: NamedExport) -> NamedExport {
834        n.specifiers = n.specifiers.fold_with(self);
835
836        n.specifiers.retain(|s| {
837            let (export_type, local_ref) = match s {
838                ExportSpecifier::Default(ExportDefaultSpecifier { exported, .. })
839                | ExportSpecifier::Namespace(ExportNamespaceSpecifier {
840                    name: ModuleExportName::Ident(exported),
841                    ..
842                }) => (ExportType::from_ident(exported), None),
843                ExportSpecifier::Named(ExportNamedSpecifier {
844                    exported: Some(ModuleExportName::Ident(exported)),
845                    orig: ModuleExportName::Ident(orig),
846                    ..
847                })
848                | ExportSpecifier::Named(ExportNamedSpecifier {
849                    orig: ModuleExportName::Ident(orig @ exported),
850                    ..
851                }) => (ExportType::from_ident(exported), Some(orig)),
852                _ => (None, None),
853            };
854
855            let Some(export_type) = export_type else {
856                return true;
857            };
858
859            let retain = self.state.should_retain_export_type(export_type);
860
861            if !retain {
862                // If the export specifier is not retained, but it refers to a local ident,
863                // we need to run again to possibly remove the local ident.
864                if let Some(local_ref) = local_ref {
865                    self.state.should_run_again = true;
866                    self.state.refs_from_removed.insert(local_ref.to_id());
867                }
868            }
869
870            self.state.should_retain_export_type(export_type)
871        });
872
873        n
874    }
875
876    fn fold_object_pat(&mut self, mut obj: ObjectPat) -> ObjectPat {
877        obj = obj.fold_children_with(self);
878
879        if !obj.props.is_empty() {
880            obj.props = take(&mut obj.props)
881                .into_iter()
882                .filter_map(|prop| match prop {
883                    ObjectPatProp::KeyValue(prop) => {
884                        if prop.value.is_invalid() {
885                            None
886                        } else {
887                            Some(ObjectPatProp::KeyValue(prop))
888                        }
889                    }
890                    ObjectPatProp::Assign(prop) => {
891                        if self.should_remove(&prop.key.to_id()) {
892                            self.mark_as_candidate(&prop.value);
893
894                            None
895                        } else {
896                            Some(ObjectPatProp::Assign(prop))
897                        }
898                    }
899                    ObjectPatProp::Rest(prop) => {
900                        if prop.arg.is_invalid() {
901                            None
902                        } else {
903                            Some(ObjectPatProp::Rest(prop))
904                        }
905                    }
906                })
907                .collect();
908        }
909
910        obj
911    }
912
913    /// This methods returns [Pat::Invalid] if the pattern should be removed.
914    fn fold_pat(&mut self, mut p: Pat) -> Pat {
915        p = p.fold_children_with(self);
916
917        if self.in_lhs_of_var {
918            match &mut p {
919                Pat::Ident(name) if self.should_remove(&name.id.to_id()) => {
920                    self.state.should_run_again = true;
921                    tracing::trace!(
922                        "Dropping var `{}{:?}` because it should be removed",
923                        name.id.sym,
924                        name.id.ctxt
925                    );
926
927                    return Pat::Invalid(Invalid { span: DUMMY_SP });
928                }
929                Pat::Array(arr) if arr.elems.is_empty() => {
930                    return Pat::Invalid(Invalid { span: DUMMY_SP });
931                }
932                Pat::Object(obj) if obj.props.is_empty() => {
933                    return Pat::Invalid(Invalid { span: DUMMY_SP });
934                }
935                Pat::Rest(rest) if rest.arg.is_invalid() => {
936                    return Pat::Invalid(Invalid { span: DUMMY_SP });
937                }
938                Pat::Expr(expr) => {
939                    if let Expr::Member(member_expr) = &**expr
940                        && let Some(id) = find_member_root_id(member_expr)
941                        && self.should_remove(&id)
942                    {
943                        self.state.should_run_again = true;
944                        tracing::trace!(
945                            "Dropping member expression object `{}{:?}` because it should be \
946                             removed",
947                            id.0,
948                            id.1
949                        );
950
951                        return Pat::Invalid(Invalid { span: DUMMY_SP });
952                    }
953                }
954                _ => {}
955            }
956        }
957
958        p
959    }
960
961    fn fold_simple_assign_target(&mut self, mut n: SimpleAssignTarget) -> SimpleAssignTarget {
962        n = n.fold_children_with(self);
963
964        if let SimpleAssignTarget::Ident(name) = &n
965            && self.should_remove(&name.id.to_id())
966        {
967            self.state.should_run_again = true;
968            tracing::trace!(
969                "Dropping var `{}{:?}` because it should be removed",
970                name.id.sym,
971                name.id.ctxt
972            );
973
974            return SimpleAssignTarget::Invalid(Invalid { span: DUMMY_SP });
975        }
976
977        if let SimpleAssignTarget::Member(member_expr) = &n
978            && let Some(id) = find_member_root_id(member_expr)
979            && self.should_remove(&id)
980        {
981            self.state.should_run_again = true;
982            tracing::trace!(
983                "Dropping member expression object `{}{:?}` because it should be removed",
984                id.0,
985                id.1
986            );
987
988            return SimpleAssignTarget::Invalid(Invalid { span: DUMMY_SP });
989        }
990
991        n
992    }
993
994    #[allow(clippy::single_match)]
995    fn fold_stmt(&mut self, mut s: Stmt) -> Stmt {
996        match s {
997            Stmt::Decl(Decl::Fn(f)) => {
998                if self.should_remove(&f.ident.to_id()) {
999                    self.mark_as_candidate(&f.function);
1000                    return Stmt::Empty(EmptyStmt { span: DUMMY_SP });
1001                }
1002
1003                s = Stmt::Decl(Decl::Fn(f));
1004            }
1005            Stmt::Decl(Decl::Class(c)) => {
1006                if self.should_remove(&c.ident.to_id()) {
1007                    self.mark_as_candidate(&c.class);
1008                    return Stmt::Empty(EmptyStmt { span: DUMMY_SP });
1009                }
1010
1011                s = Stmt::Decl(Decl::Class(c));
1012            }
1013            _ => {}
1014        }
1015
1016        self.remove_expression = false;
1017
1018        let s = s.fold_children_with(self);
1019
1020        match s {
1021            Stmt::Decl(Decl::Var(v)) if v.decls.is_empty() => {
1022                return Stmt::Empty(EmptyStmt { span: DUMMY_SP });
1023            }
1024            Stmt::Expr(_) if self.remove_expression => {
1025                self.remove_expression = false;
1026                return Stmt::Empty(EmptyStmt { span: DUMMY_SP });
1027            }
1028            _ => {}
1029        }
1030
1031        s
1032    }
1033
1034    /// This method make `name` of [VarDeclarator] to [Pat::Invalid] if it
1035    /// should be removed.
1036    fn fold_var_declarator(&mut self, d: VarDeclarator) -> VarDeclarator {
1037        let name = self.within_lhs_of_var(true, |this| d.name.clone().fold_with(this));
1038
1039        let init = self.within_lhs_of_var(false, |this| {
1040            if name.is_invalid() {
1041                this.mark_as_candidate(&d.init);
1042            }
1043            d.init.clone().fold_with(this)
1044        });
1045
1046        VarDeclarator { name, init, ..d }
1047    }
1048
1049    fn fold_var_declarators(&mut self, mut decls: Vec<VarDeclarator>) -> Vec<VarDeclarator> {
1050        decls = decls.fold_children_with(self);
1051        decls.retain(|d| !d.name.is_invalid());
1052
1053        decls
1054    }
1055
1056    // This is important for reducing binary sizes.
1057    noop_fold_type!();
1058}
1059
1060/// Returns the root identifier of a member expression.
1061///
1062/// e.g. `a.b.c` => `a`
1063fn find_member_root_id(member_expr: &MemberExpr) -> Option<Id> {
1064    match &*member_expr.obj {
1065        Expr::Member(member) => find_member_root_id(member),
1066        Expr::Ident(ident) => Some(ident.to_id()),
1067        _ => None,
1068    }
1069}