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        errors::HANDLER,
13        pass::{Repeat, Repeated},
14        DUMMY_SP,
15    },
16    ecma::{
17        ast::*,
18        visit::{fold_pass, noop_fold_type, noop_visit_type, Fold, FoldWith, Visit, VisitWith},
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                    if self.state.should_retain_export_type(export_type) {
352                        self.add_ref(local_ident.to_id());
353                    }
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) => {
661                if arr.elems.is_empty() {
662                    return AssignTargetPat::Invalid(Invalid { span: DUMMY_SP });
663                }
664            }
665            AssignTargetPat::Object(obj) => {
666                if obj.props.is_empty() {
667                    return AssignTargetPat::Invalid(Invalid { span: DUMMY_SP });
668                }
669            }
670            _ => {}
671        }
672
673        n
674    }
675
676    fn fold_expr(&mut self, e: Expr) -> Expr {
677        match e {
678            Expr::Assign(assign_expr) => {
679                let mut retain = true;
680                let left =
681                    self.within_lhs_of_var(true, |this| assign_expr.left.clone().fold_with(this));
682
683                let right = self.within_lhs_of_var(false, |this| {
684                    match left {
685                        AssignTarget::Simple(SimpleAssignTarget::Invalid(..))
686                        | AssignTarget::Pat(AssignTargetPat::Invalid(..)) => {
687                            retain = false;
688                            this.mark_as_candidate(&assign_expr.right);
689                        }
690
691                        _ => {}
692                    }
693                    assign_expr.right.clone().fold_with(this)
694                });
695
696                if retain {
697                    self.remove_expression = false;
698                    Expr::Assign(AssignExpr {
699                        left,
700                        right,
701                        ..assign_expr
702                    })
703                } else {
704                    self.remove_expression = true;
705                    *right
706                }
707            }
708            _ => {
709                self.remove_expression = false;
710                e.fold_children_with(self)
711            }
712        }
713    }
714
715    fn fold_import_decl(&mut self, mut i: ImportDecl) -> ImportDecl {
716        // Imports for side effects.
717        if i.specifiers.is_empty() {
718            return i;
719        }
720
721        let import_src = &i.src.value;
722
723        i.specifiers.retain(|s| match s {
724            ImportSpecifier::Named(ImportNamedSpecifier { local, .. })
725            | ImportSpecifier::Default(ImportDefaultSpecifier { local, .. })
726            | ImportSpecifier::Namespace(ImportStarAsSpecifier { local, .. }) => {
727                if self.should_remove(&local.to_id()) {
728                    if matches!(self.state.page_mode, PageMode::Ssr)
729                        && matches!(self.state.filter, ExportFilter::StripDataExports)
730                        // filter out non-packages import
731                        // third part packages must start with `a-z` or `@`
732                        && import_src.starts_with(|c: char| c.is_ascii_lowercase() || c == '@')
733                    {
734                        self.state
735                            .ssr_removed_packages
736                            .borrow_mut()
737                            .insert(import_src.clone());
738                    }
739                    tracing::trace!(
740                        "Dropping import `{}{:?}` because it should be removed",
741                        local.sym,
742                        local.ctxt
743                    );
744
745                    self.state.should_run_again = true;
746                    false
747                } else {
748                    true
749                }
750            }
751        });
752
753        i
754    }
755
756    fn fold_module(&mut self, m: Module) -> Module {
757        tracing::info!("ssg: Start");
758        {
759            // Fill the state.
760            let mut v = Analyzer {
761                state: &mut self.state,
762                in_lhs_of_var: false,
763                in_removed_item: false,
764            };
765            m.visit_with(&mut v);
766        }
767
768        // TODO: Use better detection logic
769        // if let PageMode::None = self.state.page_mode {
770        //     return m;
771        // }
772
773        m.fold_children_with(self)
774    }
775
776    fn fold_module_item(&mut self, i: ModuleItem) -> ModuleItem {
777        if let ModuleItem::ModuleDecl(ModuleDecl::Import(i)) = i {
778            let is_for_side_effect = i.specifiers.is_empty();
779            let i = i.fold_with(self);
780
781            if !is_for_side_effect && i.specifiers.is_empty() {
782                return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }));
783            }
784
785            return ModuleItem::ModuleDecl(ModuleDecl::Import(i));
786        }
787
788        let i = i.fold_children_with(self);
789
790        match &i {
791            ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)) if e.specifiers.is_empty() => {
792                return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }))
793            }
794            ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(e)) => match &e.decl {
795                Decl::Fn(f) => {
796                    if let Some(export_type) = self.state.export_type(&f.ident.to_id()) {
797                        if self.state.dropping_export(export_type) {
798                            tracing::trace!(
799                                "Dropping an export specifier because it's an SSR/SSG function"
800                            );
801                            return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }));
802                        }
803                    }
804                }
805
806                Decl::Var(d) => {
807                    if d.decls.is_empty() {
808                        return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }));
809                    }
810                }
811                _ => {}
812            },
813
814            ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(_))
815            | ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(_)) => {
816                if self.state.dropping_export(ExportType::Default) {
817                    tracing::trace!("Dropping an export specifier because it's a default export");
818
819                    return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }));
820                }
821            }
822            _ => {}
823        }
824
825        i
826    }
827
828    fn fold_module_items(&mut self, mut items: Vec<ModuleItem>) -> Vec<ModuleItem> {
829        items = items.fold_children_with(self);
830
831        // Drop empty nodes.
832        items.retain(|s| !matches!(s, ModuleItem::Stmt(Stmt::Empty(..))));
833
834        self.maybe_add_data_marker(&mut items);
835
836        items
837    }
838
839    fn fold_named_export(&mut self, mut n: NamedExport) -> NamedExport {
840        n.specifiers = n.specifiers.fold_with(self);
841
842        n.specifiers.retain(|s| {
843            let (export_type, local_ref) = match s {
844                ExportSpecifier::Default(ExportDefaultSpecifier { exported, .. })
845                | ExportSpecifier::Namespace(ExportNamespaceSpecifier {
846                    name: ModuleExportName::Ident(exported),
847                    ..
848                }) => (ExportType::from_ident(exported), None),
849                ExportSpecifier::Named(ExportNamedSpecifier {
850                    exported: Some(ModuleExportName::Ident(exported)),
851                    orig: ModuleExportName::Ident(orig),
852                    ..
853                })
854                | ExportSpecifier::Named(ExportNamedSpecifier {
855                    orig: ModuleExportName::Ident(orig @ exported),
856                    ..
857                }) => (ExportType::from_ident(exported), Some(orig)),
858                _ => (None, None),
859            };
860
861            let Some(export_type) = export_type else {
862                return true;
863            };
864
865            let retain = self.state.should_retain_export_type(export_type);
866
867            if !retain {
868                // If the export specifier is not retained, but it refers to a local ident,
869                // we need to run again to possibly remove the local ident.
870                if let Some(local_ref) = local_ref {
871                    self.state.should_run_again = true;
872                    self.state.refs_from_removed.insert(local_ref.to_id());
873                }
874            }
875
876            self.state.should_retain_export_type(export_type)
877        });
878
879        n
880    }
881
882    fn fold_object_pat(&mut self, mut obj: ObjectPat) -> ObjectPat {
883        obj = obj.fold_children_with(self);
884
885        if !obj.props.is_empty() {
886            obj.props = take(&mut obj.props)
887                .into_iter()
888                .filter_map(|prop| match prop {
889                    ObjectPatProp::KeyValue(prop) => {
890                        if prop.value.is_invalid() {
891                            None
892                        } else {
893                            Some(ObjectPatProp::KeyValue(prop))
894                        }
895                    }
896                    ObjectPatProp::Assign(prop) => {
897                        if self.should_remove(&prop.key.to_id()) {
898                            self.mark_as_candidate(&prop.value);
899
900                            None
901                        } else {
902                            Some(ObjectPatProp::Assign(prop))
903                        }
904                    }
905                    ObjectPatProp::Rest(prop) => {
906                        if prop.arg.is_invalid() {
907                            None
908                        } else {
909                            Some(ObjectPatProp::Rest(prop))
910                        }
911                    }
912                })
913                .collect();
914        }
915
916        obj
917    }
918
919    /// This methods returns [Pat::Invalid] if the pattern should be removed.
920    fn fold_pat(&mut self, mut p: Pat) -> Pat {
921        p = p.fold_children_with(self);
922
923        if self.in_lhs_of_var {
924            match &mut p {
925                Pat::Ident(name) => {
926                    if self.should_remove(&name.id.to_id()) {
927                        self.state.should_run_again = true;
928                        tracing::trace!(
929                            "Dropping var `{}{:?}` because it should be removed",
930                            name.id.sym,
931                            name.id.ctxt
932                        );
933
934                        return Pat::Invalid(Invalid { span: DUMMY_SP });
935                    }
936                }
937                Pat::Array(arr) => {
938                    if arr.elems.is_empty() {
939                        return Pat::Invalid(Invalid { span: DUMMY_SP });
940                    }
941                }
942                Pat::Object(obj) => {
943                    if obj.props.is_empty() {
944                        return Pat::Invalid(Invalid { span: DUMMY_SP });
945                    }
946                }
947                Pat::Rest(rest) => {
948                    if rest.arg.is_invalid() {
949                        return Pat::Invalid(Invalid { span: DUMMY_SP });
950                    }
951                }
952                Pat::Expr(expr) => {
953                    if let Expr::Member(member_expr) = &**expr {
954                        if let Some(id) = find_member_root_id(member_expr) {
955                            if self.should_remove(&id) {
956                                self.state.should_run_again = true;
957                                tracing::trace!(
958                                    "Dropping member expression object `{}{:?}` because it should \
959                                     be removed",
960                                    id.0,
961                                    id.1
962                                );
963
964                                return Pat::Invalid(Invalid { span: DUMMY_SP });
965                            }
966                        }
967                    }
968                }
969                _ => {}
970            }
971        }
972
973        p
974    }
975
976    fn fold_simple_assign_target(&mut self, mut n: SimpleAssignTarget) -> SimpleAssignTarget {
977        n = n.fold_children_with(self);
978
979        if let SimpleAssignTarget::Ident(name) = &n {
980            if self.should_remove(&name.id.to_id()) {
981                self.state.should_run_again = true;
982                tracing::trace!(
983                    "Dropping var `{}{:?}` because it should be removed",
984                    name.id.sym,
985                    name.id.ctxt
986                );
987
988                return SimpleAssignTarget::Invalid(Invalid { span: DUMMY_SP });
989            }
990        }
991
992        if let SimpleAssignTarget::Member(member_expr) = &n {
993            if let Some(id) = find_member_root_id(member_expr) {
994                if self.should_remove(&id) {
995                    self.state.should_run_again = true;
996                    tracing::trace!(
997                        "Dropping member expression object `{}{:?}` because it should be removed",
998                        id.0,
999                        id.1
1000                    );
1001
1002                    return SimpleAssignTarget::Invalid(Invalid { span: DUMMY_SP });
1003                }
1004            }
1005        }
1006
1007        n
1008    }
1009
1010    #[allow(clippy::single_match)]
1011    fn fold_stmt(&mut self, mut s: Stmt) -> Stmt {
1012        match s {
1013            Stmt::Decl(Decl::Fn(f)) => {
1014                if self.should_remove(&f.ident.to_id()) {
1015                    self.mark_as_candidate(&f.function);
1016                    return Stmt::Empty(EmptyStmt { span: DUMMY_SP });
1017                }
1018
1019                s = Stmt::Decl(Decl::Fn(f));
1020            }
1021            Stmt::Decl(Decl::Class(c)) => {
1022                if self.should_remove(&c.ident.to_id()) {
1023                    self.mark_as_candidate(&c.class);
1024                    return Stmt::Empty(EmptyStmt { span: DUMMY_SP });
1025                }
1026
1027                s = Stmt::Decl(Decl::Class(c));
1028            }
1029            _ => {}
1030        }
1031
1032        self.remove_expression = false;
1033
1034        let s = s.fold_children_with(self);
1035
1036        match s {
1037            Stmt::Decl(Decl::Var(v)) if v.decls.is_empty() => {
1038                return Stmt::Empty(EmptyStmt { span: DUMMY_SP });
1039            }
1040            Stmt::Expr(_) => {
1041                if self.remove_expression {
1042                    self.remove_expression = false;
1043                    return Stmt::Empty(EmptyStmt { span: DUMMY_SP });
1044                }
1045            }
1046            _ => {}
1047        }
1048
1049        s
1050    }
1051
1052    /// This method make `name` of [VarDeclarator] to [Pat::Invalid] if it
1053    /// should be removed.
1054    fn fold_var_declarator(&mut self, d: VarDeclarator) -> VarDeclarator {
1055        let name = self.within_lhs_of_var(true, |this| d.name.clone().fold_with(this));
1056
1057        let init = self.within_lhs_of_var(false, |this| {
1058            if name.is_invalid() {
1059                this.mark_as_candidate(&d.init);
1060            }
1061            d.init.clone().fold_with(this)
1062        });
1063
1064        VarDeclarator { name, init, ..d }
1065    }
1066
1067    fn fold_var_declarators(&mut self, mut decls: Vec<VarDeclarator>) -> Vec<VarDeclarator> {
1068        decls = decls.fold_children_with(self);
1069        decls.retain(|d| !d.name.is_invalid());
1070
1071        decls
1072    }
1073
1074    // This is important for reducing binary sizes.
1075    noop_fold_type!();
1076}
1077
1078/// Returns the root identifier of a member expression.
1079///
1080/// e.g. `a.b.c` => `a`
1081fn find_member_root_id(member_expr: &MemberExpr) -> Option<Id> {
1082    match &*member_expr.obj {
1083        Expr::Member(member) => find_member_root_id(member),
1084        Expr::Ident(ident) => Some(ident.to_id()),
1085        _ => None,
1086    }
1087}