next_custom_transforms/transforms/
next_ssg.rs

1use std::{cell::RefCell, mem::take, rc::Rc};
2
3use easy_error::{bail, Error};
4use rustc_hash::FxHashSet;
5use swc_core::{
6    atoms::Atom,
7    common::{
8        errors::HANDLER,
9        pass::{Repeat, Repeated},
10        DUMMY_SP,
11    },
12    ecma::{
13        ast::*,
14        visit::{fold_pass, noop_fold_type, Fold, FoldWith},
15    },
16};
17
18static SSG_EXPORTS: &[&str; 3] = &["getStaticProps", "getStaticPaths", "getServerSideProps"];
19
20/// Note: This paths requires running `resolver` **before** running this.
21pub fn next_ssg(eliminated_packages: Rc<RefCell<FxHashSet<Atom>>>) -> impl Pass {
22    fold_pass(Repeat::new(NextSsg {
23        state: State {
24            eliminated_packages,
25            ..Default::default()
26        },
27        in_lhs_of_var: false,
28    }))
29}
30
31/// State of the transforms. Shared by the analyzer and the transform.
32#[derive(Debug, Default)]
33struct State {
34    /// Identifiers referenced by non-data function codes.
35    ///
36    /// Cleared before running each pass, because we drop ast nodes between the
37    /// passes.
38    refs_from_other: FxHashSet<Id>,
39
40    /// Identifiers referenced by data functions or derivatives.
41    ///
42    /// Preserved between runs, because we should remember derivatives of data
43    /// functions as the data function itself is already removed.
44    refs_from_data_fn: FxHashSet<Id>,
45
46    cur_declaring: FxHashSet<Id>,
47
48    is_prerenderer: bool,
49    is_server_props: bool,
50    done: bool,
51
52    should_run_again: bool,
53
54    /// Track the import packages which are eliminated in the
55    /// `getServerSideProps`
56    pub eliminated_packages: Rc<RefCell<FxHashSet<Atom>>>,
57}
58
59impl State {
60    #[allow(clippy::wrong_self_convention)]
61    fn is_data_identifier(&mut self, i: &Ident) -> Result<bool, Error> {
62        if SSG_EXPORTS.contains(&&*i.sym) {
63            if &*i.sym == "getServerSideProps" {
64                if self.is_prerenderer {
65                    HANDLER.with(|handler| {
66                        handler
67                            .struct_span_err(
68                                i.span,
69                                "You can not use getStaticProps or getStaticPaths with \
70                                 getServerSideProps. To use SSG, please remove getServerSideProps",
71                            )
72                            .emit()
73                    });
74                    bail!("both ssg and ssr functions present");
75                }
76
77                self.is_server_props = true;
78            } else {
79                if self.is_server_props {
80                    HANDLER.with(|handler| {
81                        handler
82                            .struct_span_err(
83                                i.span,
84                                "You can not use getStaticProps or getStaticPaths with \
85                                 getServerSideProps. To use SSG, please remove getServerSideProps",
86                            )
87                            .emit()
88                    });
89                    bail!("both ssg and ssr functions present");
90                }
91
92                self.is_prerenderer = true;
93            }
94
95            Ok(true)
96        } else {
97            Ok(false)
98        }
99    }
100}
101
102struct Analyzer<'a> {
103    state: &'a mut State,
104    in_lhs_of_var: bool,
105    in_data_fn: bool,
106}
107
108impl Analyzer<'_> {
109    fn add_ref(&mut self, id: Id) {
110        tracing::trace!("add_ref({}{:?}, data = {})", id.0, id.1, self.in_data_fn);
111        if self.in_data_fn {
112            self.state.refs_from_data_fn.insert(id);
113        } else {
114            if self.state.cur_declaring.contains(&id) {
115                return;
116            }
117
118            self.state.refs_from_other.insert(id);
119        }
120    }
121}
122
123impl Fold for Analyzer<'_> {
124    // This is important for reducing binary sizes.
125    noop_fold_type!();
126
127    fn fold_binding_ident(&mut self, i: BindingIdent) -> BindingIdent {
128        if !self.in_lhs_of_var || self.in_data_fn {
129            self.add_ref(i.id.to_id());
130        }
131
132        i
133    }
134
135    fn fold_export_named_specifier(&mut self, s: ExportNamedSpecifier) -> ExportNamedSpecifier {
136        if let ModuleExportName::Ident(id) = &s.orig {
137            if !SSG_EXPORTS.contains(&&*id.sym) {
138                self.add_ref(id.to_id());
139            }
140        }
141
142        s
143    }
144
145    fn fold_export_decl(&mut self, s: ExportDecl) -> ExportDecl {
146        if let Decl::Var(d) = &s.decl {
147            if d.decls.is_empty() {
148                return s;
149            }
150
151            if let Pat::Ident(id) = &d.decls[0].name {
152                if !SSG_EXPORTS.contains(&&*id.id.sym) {
153                    self.add_ref(id.to_id());
154                }
155            }
156        }
157
158        s.fold_children_with(self)
159    }
160
161    fn fold_expr(&mut self, e: Expr) -> Expr {
162        let e = e.fold_children_with(self);
163
164        if let Expr::Ident(i) = &e {
165            self.add_ref(i.to_id());
166        }
167
168        e
169    }
170
171    fn fold_jsx_element(&mut self, jsx: JSXElement) -> JSXElement {
172        fn get_leftmost_id_member_expr(e: &JSXMemberExpr) -> Id {
173            match &e.obj {
174                JSXObject::Ident(i) => i.to_id(),
175                JSXObject::JSXMemberExpr(e) => get_leftmost_id_member_expr(e),
176            }
177        }
178
179        match &jsx.opening.name {
180            JSXElementName::Ident(i) => {
181                self.add_ref(i.to_id());
182            }
183            JSXElementName::JSXMemberExpr(e) => {
184                self.add_ref(get_leftmost_id_member_expr(e));
185            }
186            _ => {}
187        }
188
189        jsx.fold_children_with(self)
190    }
191
192    fn fold_fn_decl(&mut self, f: FnDecl) -> FnDecl {
193        let old_in_data = self.in_data_fn;
194
195        self.state.cur_declaring.insert(f.ident.to_id());
196
197        if let Ok(is_data_identifier) = self.state.is_data_identifier(&f.ident) {
198            self.in_data_fn |= is_data_identifier;
199        } else {
200            return f;
201        }
202        tracing::trace!(
203            "ssg: Handling `{}{:?}`; in_data_fn = {:?}",
204            f.ident.sym,
205            f.ident.ctxt,
206            self.in_data_fn
207        );
208
209        let f = f.fold_children_with(self);
210
211        self.state.cur_declaring.remove(&f.ident.to_id());
212
213        self.in_data_fn = old_in_data;
214
215        f
216    }
217
218    fn fold_fn_expr(&mut self, f: FnExpr) -> FnExpr {
219        let f = f.fold_children_with(self);
220
221        if let Some(id) = &f.ident {
222            self.add_ref(id.to_id());
223        }
224
225        f
226    }
227
228    /// Drops [ExportDecl] if all specifiers are removed.
229    fn fold_module_item(&mut self, s: ModuleItem) -> ModuleItem {
230        match s {
231            ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)) if !e.specifiers.is_empty() => {
232                let e = e.fold_with(self);
233
234                if e.specifiers.is_empty() {
235                    return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }));
236                }
237
238                return ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e));
239            }
240            _ => {}
241        };
242
243        // Visit children to ensure that all references is added to the scope.
244        let s = s.fold_children_with(self);
245
246        if let ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(e)) = &s {
247            match &e.decl {
248                Decl::Fn(f) => {
249                    // Drop getStaticProps.
250                    if let Ok(is_data_identifier) = self.state.is_data_identifier(&f.ident) {
251                        if is_data_identifier {
252                            return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }));
253                        }
254                    } else {
255                        return s;
256                    }
257                }
258
259                Decl::Var(d) => {
260                    if d.decls.is_empty() {
261                        return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }));
262                    }
263                }
264                _ => {}
265            }
266        }
267
268        s
269    }
270
271    fn fold_named_export(&mut self, mut n: NamedExport) -> NamedExport {
272        if n.src.is_some() {
273            n.specifiers = n.specifiers.fold_with(self);
274        }
275
276        n
277    }
278
279    fn fold_prop(&mut self, p: Prop) -> Prop {
280        let p = p.fold_children_with(self);
281
282        if let Prop::Shorthand(i) = &p {
283            self.add_ref(i.to_id());
284        }
285
286        p
287    }
288
289    fn fold_var_declarator(&mut self, mut v: VarDeclarator) -> VarDeclarator {
290        let old_in_data = self.in_data_fn;
291
292        if let Pat::Ident(name) = &v.name {
293            if let Ok(is_data_identifier) = self.state.is_data_identifier(&name.id) {
294                if is_data_identifier {
295                    self.in_data_fn = true;
296                }
297            } else {
298                return v;
299            }
300        }
301
302        let old_in_lhs_of_var = self.in_lhs_of_var;
303
304        self.in_lhs_of_var = true;
305        v.name = v.name.fold_with(self);
306
307        self.in_lhs_of_var = false;
308        v.init = v.init.fold_with(self);
309
310        self.in_lhs_of_var = old_in_lhs_of_var;
311
312        self.in_data_fn = old_in_data;
313
314        v
315    }
316}
317
318/// Actual implementation of the transform.
319struct NextSsg {
320    pub state: State,
321    in_lhs_of_var: bool,
322}
323
324impl NextSsg {
325    fn should_remove(&self, id: Id) -> bool {
326        self.state.refs_from_data_fn.contains(&id) && !self.state.refs_from_other.contains(&id)
327    }
328
329    /// Mark identifiers in `n` as a candidate for removal.
330    fn mark_as_candidate<N>(&mut self, n: N) -> N
331    where
332        N: for<'aa> FoldWith<Analyzer<'aa>>,
333    {
334        tracing::debug!("mark_as_candidate");
335
336        // Analyzer never change `in_data_fn` to false, so all identifiers in `n` will
337        // be marked as referenced from a data function.
338        let mut v = Analyzer {
339            state: &mut self.state,
340            in_lhs_of_var: false,
341            in_data_fn: true,
342        };
343
344        let n = n.fold_with(&mut v);
345        self.state.should_run_again = true;
346        n
347    }
348}
349
350impl Repeated for NextSsg {
351    fn changed(&self) -> bool {
352        self.state.should_run_again
353    }
354
355    fn reset(&mut self) {
356        self.state.refs_from_other.clear();
357        self.state.cur_declaring.clear();
358        self.state.should_run_again = false;
359    }
360}
361
362/// `VisitMut` is faster than [Fold], but we use [Fold] because it's much easier
363/// to read.
364///
365/// Note: We don't implement `fold_script` because next.js doesn't use it.
366impl Fold for NextSsg {
367    // This is important for reducing binary sizes.
368    noop_fold_type!();
369
370    fn fold_import_decl(&mut self, mut i: ImportDecl) -> ImportDecl {
371        // Imports for side effects.
372        if i.specifiers.is_empty() {
373            return i;
374        }
375
376        let import_src = &i.src.value;
377
378        i.specifiers.retain(|s| match s {
379            ImportSpecifier::Named(ImportNamedSpecifier { local, .. })
380            | ImportSpecifier::Default(ImportDefaultSpecifier { local, .. })
381            | ImportSpecifier::Namespace(ImportStarAsSpecifier { local, .. }) => {
382                if self.should_remove(local.to_id()) {
383                    if self.state.is_server_props
384                        // filter out non-packages import
385                        // third part packages must start with `a-z` or `@`
386                        && import_src.starts_with(|c: char| c.is_ascii_lowercase() || c == '@')
387                    {
388                        self.state
389                            .eliminated_packages
390                            .borrow_mut()
391                            .insert(import_src.clone());
392                    }
393                    tracing::trace!(
394                        "Dropping import `{}{:?}` because it should be removed",
395                        local.sym,
396                        local.ctxt
397                    );
398
399                    self.state.should_run_again = true;
400                    false
401                } else {
402                    true
403                }
404            }
405        });
406
407        i
408    }
409
410    fn fold_module(&mut self, mut m: Module) -> Module {
411        tracing::info!("ssg: Start");
412        {
413            // Fill the state.
414            let mut v = Analyzer {
415                state: &mut self.state,
416                in_lhs_of_var: false,
417                in_data_fn: false,
418            };
419            m = m.fold_with(&mut v);
420        }
421
422        // TODO: Use better detection logic
423        // if !self.state.is_prerenderer && !self.state.is_server_props {
424        //     return m;
425        // }
426
427        m.fold_children_with(self)
428    }
429
430    fn fold_module_item(&mut self, i: ModuleItem) -> ModuleItem {
431        if let ModuleItem::ModuleDecl(ModuleDecl::Import(i)) = i {
432            let is_for_side_effect = i.specifiers.is_empty();
433            let i = i.fold_with(self);
434
435            if !is_for_side_effect && i.specifiers.is_empty() {
436                return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }));
437            }
438
439            return ModuleItem::ModuleDecl(ModuleDecl::Import(i));
440        }
441
442        let i = i.fold_children_with(self);
443
444        match &i {
445            ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)) if e.specifiers.is_empty() => {
446                return ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }))
447            }
448            _ => {}
449        }
450
451        i
452    }
453
454    fn fold_module_items(&mut self, mut items: Vec<ModuleItem>) -> Vec<ModuleItem> {
455        items = items.fold_children_with(self);
456
457        // Drop nodes.
458        items.retain(|s| !matches!(s, ModuleItem::Stmt(Stmt::Empty(..))));
459
460        if !self.state.done
461            && !self.state.should_run_again
462            && (self.state.is_prerenderer || self.state.is_server_props)
463        {
464            self.state.done = true;
465
466            if items.iter().any(|s| s.is_module_decl()) {
467                let mut var = Some(VarDeclarator {
468                    span: DUMMY_SP,
469                    name: Pat::Ident(
470                        IdentName::new(
471                            if self.state.is_prerenderer {
472                                "__N_SSG".into()
473                            } else {
474                                "__N_SSP".into()
475                            },
476                            DUMMY_SP,
477                        )
478                        .into(),
479                    ),
480                    init: Some(Box::new(Expr::Lit(Lit::Bool(Bool {
481                        span: DUMMY_SP,
482                        value: true,
483                    })))),
484                    definite: Default::default(),
485                });
486
487                let mut new = Vec::with_capacity(items.len() + 1);
488                for item in take(&mut items) {
489                    if let ModuleItem::ModuleDecl(
490                        ModuleDecl::ExportNamed(..)
491                        | ModuleDecl::ExportDecl(..)
492                        | ModuleDecl::ExportDefaultDecl(..)
493                        | ModuleDecl::ExportDefaultExpr(..),
494                    ) = &item
495                    {
496                        if let Some(var) = var.take() {
497                            new.push(ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
498                                span: DUMMY_SP,
499                                decl: Decl::Var(Box::new(VarDecl {
500                                    span: DUMMY_SP,
501                                    kind: VarDeclKind::Var,
502                                    decls: vec![var],
503                                    ..Default::default()
504                                })),
505                            })))
506                        }
507                    }
508
509                    new.push(item);
510                }
511
512                return new;
513            }
514        }
515
516        items
517    }
518
519    fn fold_named_export(&mut self, mut n: NamedExport) -> NamedExport {
520        n.specifiers = n.specifiers.fold_with(self);
521
522        n.specifiers.retain(|s| {
523            let preserve = match s {
524                ExportSpecifier::Namespace(ExportNamespaceSpecifier {
525                    name: ModuleExportName::Ident(exported),
526                    ..
527                })
528                | ExportSpecifier::Default(ExportDefaultSpecifier { exported, .. })
529                | ExportSpecifier::Named(ExportNamedSpecifier {
530                    exported: Some(ModuleExportName::Ident(exported)),
531                    ..
532                }) => self
533                    .state
534                    .is_data_identifier(exported)
535                    .map(|is_data_identifier| !is_data_identifier),
536                ExportSpecifier::Named(ExportNamedSpecifier {
537                    orig: ModuleExportName::Ident(orig),
538                    ..
539                }) => self
540                    .state
541                    .is_data_identifier(orig)
542                    .map(|is_data_identifier| !is_data_identifier),
543
544                _ => Ok(true),
545            };
546
547            match preserve {
548                Ok(false) => {
549                    tracing::trace!("Dropping a export specifier because it's a data identifier");
550
551                    if let ExportSpecifier::Named(ExportNamedSpecifier {
552                        orig: ModuleExportName::Ident(orig),
553                        ..
554                    }) = s
555                    {
556                        self.state.should_run_again = true;
557                        self.state.refs_from_data_fn.insert(orig.to_id());
558                    }
559
560                    false
561                }
562                Ok(true) => true,
563                Err(_) => false,
564            }
565        });
566
567        n
568    }
569
570    /// This methods returns [Pat::Invalid] if the pattern should be removed.
571    fn fold_pat(&mut self, mut p: Pat) -> Pat {
572        p = p.fold_children_with(self);
573
574        if self.in_lhs_of_var {
575            match &mut p {
576                Pat::Ident(name) => {
577                    if self.should_remove(name.id.to_id()) {
578                        self.state.should_run_again = true;
579                        tracing::trace!(
580                            "Dropping var `{}{:?}` because it should be removed",
581                            name.id.sym,
582                            name.id.ctxt
583                        );
584
585                        return Pat::Invalid(Invalid { span: DUMMY_SP });
586                    }
587                }
588                Pat::Array(arr) => {
589                    if !arr.elems.is_empty() {
590                        arr.elems.retain(|e| !matches!(e, Some(Pat::Invalid(..))));
591
592                        if arr.elems.is_empty() {
593                            return Pat::Invalid(Invalid { span: DUMMY_SP });
594                        }
595                    }
596                }
597                Pat::Object(obj) => {
598                    if !obj.props.is_empty() {
599                        obj.props = take(&mut obj.props)
600                            .into_iter()
601                            .filter_map(|prop| match prop {
602                                ObjectPatProp::KeyValue(prop) => {
603                                    if prop.value.is_invalid() {
604                                        None
605                                    } else {
606                                        Some(ObjectPatProp::KeyValue(prop))
607                                    }
608                                }
609                                ObjectPatProp::Assign(prop) => {
610                                    if self.should_remove(prop.key.to_id()) {
611                                        self.mark_as_candidate(prop.value);
612
613                                        None
614                                    } else {
615                                        Some(ObjectPatProp::Assign(prop))
616                                    }
617                                }
618                                ObjectPatProp::Rest(prop) => {
619                                    if prop.arg.is_invalid() {
620                                        None
621                                    } else {
622                                        Some(ObjectPatProp::Rest(prop))
623                                    }
624                                }
625                            })
626                            .collect();
627
628                        if obj.props.is_empty() {
629                            return Pat::Invalid(Invalid { span: DUMMY_SP });
630                        }
631                    }
632                }
633                Pat::Rest(rest) => {
634                    if rest.arg.is_invalid() {
635                        return Pat::Invalid(Invalid { span: DUMMY_SP });
636                    }
637                }
638                _ => {}
639            }
640        }
641
642        p
643    }
644
645    #[allow(clippy::single_match)]
646    fn fold_stmt(&mut self, mut s: Stmt) -> Stmt {
647        match s {
648            Stmt::Decl(Decl::Fn(f)) => {
649                if self.should_remove(f.ident.to_id()) {
650                    self.mark_as_candidate(f.function);
651                    return Stmt::Empty(EmptyStmt { span: DUMMY_SP });
652                }
653
654                s = Stmt::Decl(Decl::Fn(f));
655            }
656            _ => {}
657        }
658
659        let s = s.fold_children_with(self);
660        match s {
661            Stmt::Decl(Decl::Var(v)) if v.decls.is_empty() => {
662                return Stmt::Empty(EmptyStmt { span: DUMMY_SP });
663            }
664            _ => {}
665        }
666
667        s
668    }
669
670    /// This method make `name` of [VarDeclarator] to [Pat::Invalid] if it
671    /// should be removed.
672    fn fold_var_declarator(&mut self, mut d: VarDeclarator) -> VarDeclarator {
673        let old = self.in_lhs_of_var;
674        self.in_lhs_of_var = true;
675        let name = d.name.fold_with(self);
676
677        self.in_lhs_of_var = false;
678        if name.is_invalid() {
679            d.init = self.mark_as_candidate(d.init);
680        }
681        let init = d.init.fold_with(self);
682        self.in_lhs_of_var = old;
683
684        VarDeclarator { name, init, ..d }
685    }
686
687    fn fold_var_declarators(&mut self, mut decls: Vec<VarDeclarator>) -> Vec<VarDeclarator> {
688        decls = decls.fold_children_with(self);
689        decls.retain(|d| !d.name.is_invalid());
690
691        decls
692    }
693}