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