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, Atom},
7    common::{
8        errors::HANDLER,
9        pass::{Repeat, Repeated},
10        DUMMY_SP,
11    },
12    ecma::{
13        ast::*,
14        visit::{noop_visit_mut_type, visit_mut_pass, VisitMut, VisitMutWith},
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    visit_mut_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 VisitMut for Analyzer<'_> {
124    // This is important for reducing binary sizes.
125    noop_visit_mut_type!();
126
127    fn visit_mut_binding_ident(&mut self, i: &mut BindingIdent) {
128        if !self.in_lhs_of_var || self.in_data_fn {
129            self.add_ref(i.id.to_id());
130        }
131    }
132
133    fn visit_mut_export_named_specifier(&mut self, s: &mut ExportNamedSpecifier) {
134        if let ModuleExportName::Ident(id) = &s.orig {
135            if !SSG_EXPORTS.contains(&&*id.sym) {
136                self.add_ref(id.to_id());
137            }
138        }
139    }
140
141    fn visit_mut_export_decl(&mut self, s: &mut ExportDecl) {
142        if let Decl::Var(d) = &s.decl {
143            if d.decls.is_empty() {
144                return;
145            }
146
147            for decl in &d.decls {
148                if let Pat::Ident(id) = &decl.name {
149                    if !SSG_EXPORTS.contains(&&*id.id.sym) {
150                        self.add_ref(id.to_id());
151                    }
152                }
153            }
154        }
155
156        s.visit_mut_children_with(self)
157    }
158
159    fn visit_mut_expr(&mut self, e: &mut Expr) {
160        e.visit_mut_children_with(self);
161
162        if let Expr::Ident(i) = &e {
163            self.add_ref(i.to_id());
164        }
165    }
166
167    fn visit_mut_jsx_element(&mut self, jsx: &mut JSXElement) {
168        fn get_leftmost_id_member_expr(e: &JSXMemberExpr) -> Id {
169            match &e.obj {
170                JSXObject::Ident(i) => i.to_id(),
171                JSXObject::JSXMemberExpr(e) => get_leftmost_id_member_expr(e),
172            }
173        }
174
175        match &jsx.opening.name {
176            JSXElementName::Ident(i) => {
177                self.add_ref(i.to_id());
178            }
179            JSXElementName::JSXMemberExpr(e) => {
180                self.add_ref(get_leftmost_id_member_expr(e));
181            }
182            _ => {}
183        }
184
185        jsx.visit_mut_children_with(self);
186    }
187
188    fn visit_mut_fn_decl(&mut self, f: &mut FnDecl) {
189        let old_in_data = self.in_data_fn;
190
191        self.state.cur_declaring.insert(f.ident.to_id());
192
193        if let Ok(is_data_identifier) = self.state.is_data_identifier(&f.ident) {
194            self.in_data_fn |= is_data_identifier;
195        } else {
196            return;
197        }
198        tracing::trace!(
199            "ssg: Handling `{}{:?}`; in_data_fn = {:?}",
200            f.ident.sym,
201            f.ident.ctxt,
202            self.in_data_fn
203        );
204
205        f.visit_mut_children_with(self);
206
207        self.state.cur_declaring.remove(&f.ident.to_id());
208
209        self.in_data_fn = old_in_data;
210    }
211
212    fn visit_mut_fn_expr(&mut self, f: &mut FnExpr) {
213        f.visit_mut_children_with(self);
214
215        if let Some(id) = &f.ident {
216            self.add_ref(id.to_id());
217        }
218    }
219
220    /// Drops [ExportDecl] if all specifiers are removed.
221    fn visit_mut_module_item(&mut self, s: &mut ModuleItem) {
222        match s {
223            ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)) if !e.specifiers.is_empty() => {
224                e.visit_mut_with(self);
225
226                if e.specifiers.is_empty() {
227                    *s = ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }));
228                    return;
229                }
230
231                return;
232            }
233            _ => {}
234        };
235
236        // Visit children to ensure that all references is added to the scope.
237        s.visit_mut_children_with(self);
238
239        if let ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(e)) = &s {
240            match &e.decl {
241                Decl::Fn(f) => {
242                    // Drop getStaticProps.
243                    if let Ok(is_data_identifier) = self.state.is_data_identifier(&f.ident) {
244                        if is_data_identifier {
245                            *s = ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }));
246                        }
247                    }
248                }
249
250                Decl::Var(d) => {
251                    if d.decls.is_empty() {
252                        *s = ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }));
253                    }
254                }
255                _ => {}
256            }
257        }
258    }
259
260    fn visit_mut_named_export(&mut self, n: &mut NamedExport) {
261        if n.src.is_some() {
262            n.specifiers.visit_mut_with(self);
263        }
264    }
265
266    fn visit_mut_prop(&mut self, p: &mut Prop) {
267        p.visit_mut_children_with(self);
268
269        if let Prop::Shorthand(i) = &p {
270            self.add_ref(i.to_id());
271        }
272    }
273
274    fn visit_mut_var_declarator(&mut self, v: &mut VarDeclarator) {
275        let old_in_data = self.in_data_fn;
276
277        if let Pat::Ident(name) = &v.name {
278            if let Ok(is_data_identifier) = self.state.is_data_identifier(&name.id) {
279                if is_data_identifier {
280                    self.in_data_fn = true;
281                }
282            } else {
283                return;
284            }
285        }
286
287        let old_in_lhs_of_var = self.in_lhs_of_var;
288
289        self.in_lhs_of_var = true;
290        v.name.visit_mut_with(self);
291
292        self.in_lhs_of_var = false;
293        v.init.visit_mut_with(self);
294
295        self.in_lhs_of_var = old_in_lhs_of_var;
296
297        self.in_data_fn = old_in_data;
298    }
299}
300
301/// Actual implementation of the transform.
302struct NextSsg {
303    pub state: State,
304    in_lhs_of_var: bool,
305}
306
307impl NextSsg {
308    fn should_remove(&self, id: Id) -> bool {
309        self.state.refs_from_data_fn.contains(&id) && !self.state.refs_from_other.contains(&id)
310    }
311
312    /// Mark identifiers in `n` as a candidate for removal.
313    fn mark_as_candidate<N>(&mut self, n: &mut N)
314    where
315        N: for<'aa> VisitMutWith<Analyzer<'aa>>,
316    {
317        tracing::debug!("mark_as_candidate");
318
319        // Analyzer never change `in_data_fn` to false, so all identifiers in `n` will
320        // be marked as referenced from a data function.
321        let mut v = Analyzer {
322            state: &mut self.state,
323            in_lhs_of_var: false,
324            in_data_fn: true,
325        };
326
327        n.visit_mut_with(&mut v);
328        self.state.should_run_again = true;
329    }
330}
331
332impl Repeated for NextSsg {
333    fn changed(&self) -> bool {
334        self.state.should_run_again
335    }
336
337    fn reset(&mut self) {
338        self.state.refs_from_other.clear();
339        self.state.cur_declaring.clear();
340        self.state.should_run_again = false;
341    }
342}
343
344/// Note: We don't implement `visit_mut_script` because next.js doesn't use it.
345impl VisitMut for NextSsg {
346    // This is important for reducing binary sizes.
347    noop_visit_mut_type!();
348
349    fn visit_mut_import_decl(&mut self, i: &mut ImportDecl) {
350        // Imports for side effects.
351        if i.specifiers.is_empty() {
352            return;
353        }
354
355        let import_src = &i.src.value;
356
357        i.specifiers.retain(|s| match s {
358            ImportSpecifier::Named(ImportNamedSpecifier { local, .. })
359            | ImportSpecifier::Default(ImportDefaultSpecifier { local, .. })
360            | ImportSpecifier::Namespace(ImportStarAsSpecifier { local, .. }) => {
361                if self.should_remove(local.to_id()) {
362                    if self.state.is_server_props
363                        // filter out non-packages import
364                        // third part packages must start with `a-z` or `@`
365                        && import_src.as_str().unwrap_or_default().starts_with(|c: char| c.is_ascii_lowercase() || c == '@')
366                    {
367                        self.state
368                            .eliminated_packages
369                            .borrow_mut()
370                            .insert(import_src.clone().to_atom_lossy().into_owned());
371                    }
372                    tracing::trace!(
373                        "Dropping import `{}{:?}` because it should be removed",
374                        local.sym,
375                        local.ctxt
376                    );
377
378                    self.state.should_run_again = true;
379                    false
380                } else {
381                    true
382                }
383            }
384        });
385    }
386
387    fn visit_mut_module(&mut self, m: &mut Module) {
388        tracing::info!("ssg: Start");
389        {
390            // Fill the state.
391            let mut v = Analyzer {
392                state: &mut self.state,
393                in_lhs_of_var: false,
394                in_data_fn: false,
395            };
396            m.visit_mut_with(&mut v);
397        }
398
399        // TODO: Use better detection logic
400        // if !self.state.is_prerenderer && !self.state.is_server_props {
401        //     return m;
402        // }
403
404        m.visit_mut_children_with(self)
405    }
406
407    fn visit_mut_module_item(&mut self, i: &mut ModuleItem) {
408        if let ModuleItem::ModuleDecl(ModuleDecl::Import(decl)) = i {
409            let is_for_side_effect = decl.specifiers.is_empty();
410            decl.visit_mut_with(self);
411
412            if !is_for_side_effect && decl.specifiers.is_empty() {
413                *i = ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }));
414                return;
415            }
416
417            return;
418        }
419
420        i.visit_mut_children_with(self);
421
422        match &i {
423            ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)) if e.specifiers.is_empty() => {
424                *i = ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }));
425            }
426            _ => {}
427        }
428    }
429
430    fn visit_mut_module_items(&mut self, items: &mut Vec<ModuleItem>) {
431        items.visit_mut_children_with(self);
432
433        // Drop nodes.
434        items.retain(|s| !matches!(s, ModuleItem::Stmt(Stmt::Empty(..))));
435
436        if !self.state.done
437            && !self.state.should_run_again
438            && (self.state.is_prerenderer || self.state.is_server_props)
439        {
440            self.state.done = true;
441
442            if items.iter().any(|s| s.is_module_decl()) {
443                let mut var = Some(VarDeclarator {
444                    span: DUMMY_SP,
445                    name: Pat::Ident(
446                        IdentName::new(
447                            if self.state.is_prerenderer {
448                                atom!("__N_SSG")
449                            } else {
450                                atom!("__N_SSP")
451                            },
452                            DUMMY_SP,
453                        )
454                        .into(),
455                    ),
456                    init: Some(Box::new(Expr::Lit(Lit::Bool(Bool {
457                        span: DUMMY_SP,
458                        value: true,
459                    })))),
460                    definite: Default::default(),
461                });
462
463                let mut new = Vec::with_capacity(items.len() + 1);
464                for item in take(items) {
465                    if let ModuleItem::ModuleDecl(
466                        ModuleDecl::ExportNamed(..)
467                        | ModuleDecl::ExportDecl(..)
468                        | ModuleDecl::ExportDefaultDecl(..)
469                        | ModuleDecl::ExportDefaultExpr(..),
470                    ) = &item
471                    {
472                        if let Some(var) = var.take() {
473                            new.push(ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
474                                span: DUMMY_SP,
475                                decl: Decl::Var(Box::new(VarDecl {
476                                    span: DUMMY_SP,
477                                    kind: VarDeclKind::Var,
478                                    decls: vec![var],
479                                    ..Default::default()
480                                })),
481                            })))
482                        }
483                    }
484
485                    new.push(item);
486                }
487
488                *items = new;
489            }
490        }
491    }
492
493    fn visit_mut_named_export(&mut self, n: &mut NamedExport) {
494        n.specifiers.visit_mut_with(self);
495
496        n.specifiers.retain(|s| {
497            let preserve = match s {
498                ExportSpecifier::Namespace(ExportNamespaceSpecifier {
499                    name: ModuleExportName::Ident(exported),
500                    ..
501                })
502                | ExportSpecifier::Default(ExportDefaultSpecifier { exported, .. })
503                | ExportSpecifier::Named(ExportNamedSpecifier {
504                    exported: Some(ModuleExportName::Ident(exported)),
505                    ..
506                }) => self
507                    .state
508                    .is_data_identifier(exported)
509                    .map(|is_data_identifier| !is_data_identifier),
510                ExportSpecifier::Named(ExportNamedSpecifier {
511                    orig: ModuleExportName::Ident(orig),
512                    ..
513                }) => self
514                    .state
515                    .is_data_identifier(orig)
516                    .map(|is_data_identifier| !is_data_identifier),
517
518                _ => Ok(true),
519            };
520
521            match preserve {
522                Ok(false) => {
523                    tracing::trace!("Dropping a export specifier because it's a data identifier");
524
525                    if let ExportSpecifier::Named(ExportNamedSpecifier {
526                        orig: ModuleExportName::Ident(orig),
527                        ..
528                    }) = s
529                    {
530                        self.state.should_run_again = true;
531                        self.state.refs_from_data_fn.insert(orig.to_id());
532                    }
533
534                    false
535                }
536                Ok(true) => true,
537                Err(_) => false,
538            }
539        });
540    }
541
542    /// This methods returns [Pat::Invalid] if the pattern should be removed.
543    fn visit_mut_pat(&mut self, p: &mut Pat) {
544        p.visit_mut_children_with(self);
545
546        if self.in_lhs_of_var {
547            match p {
548                Pat::Ident(name) => {
549                    if self.should_remove(name.id.to_id()) {
550                        self.state.should_run_again = true;
551                        tracing::trace!(
552                            "Dropping var `{}{:?}` because it should be removed",
553                            name.id.sym,
554                            name.id.ctxt
555                        );
556
557                        *p = Pat::Invalid(Invalid { span: DUMMY_SP });
558                    }
559                }
560                Pat::Array(arr) => {
561                    if !arr.elems.is_empty() {
562                        arr.elems.retain(|e| !matches!(e, Some(Pat::Invalid(..))));
563
564                        if arr.elems.is_empty() {
565                            *p = Pat::Invalid(Invalid { span: DUMMY_SP });
566                        }
567                    }
568                }
569                Pat::Object(obj) => {
570                    if !obj.props.is_empty() {
571                        obj.props.retain_mut(|prop| match prop {
572                            ObjectPatProp::KeyValue(prop) => !prop.value.is_invalid(),
573                            ObjectPatProp::Assign(prop) => {
574                                if self.should_remove(prop.key.to_id()) {
575                                    self.mark_as_candidate(&mut prop.value);
576
577                                    false
578                                } else {
579                                    true
580                                }
581                            }
582                            ObjectPatProp::Rest(prop) => !prop.arg.is_invalid(),
583                        });
584
585                        if obj.props.is_empty() {
586                            *p = Pat::Invalid(Invalid { span: DUMMY_SP });
587                        }
588                    }
589                }
590                Pat::Rest(rest) => {
591                    if rest.arg.is_invalid() {
592                        *p = Pat::Invalid(Invalid { span: DUMMY_SP });
593                    }
594                }
595                _ => {}
596            }
597        }
598    }
599
600    #[allow(clippy::single_match)]
601    fn visit_mut_stmt(&mut self, s: &mut Stmt) {
602        if let Stmt::Decl(Decl::Fn(f)) = s {
603            if self.should_remove(f.ident.to_id()) {
604                self.mark_as_candidate(&mut f.function);
605                *s = Stmt::Empty(EmptyStmt { span: DUMMY_SP });
606                return;
607            }
608        }
609
610        s.visit_mut_children_with(self);
611        match s {
612            Stmt::Decl(Decl::Var(v)) if v.decls.is_empty() => {
613                *s = Stmt::Empty(EmptyStmt { span: DUMMY_SP });
614            }
615            _ => {}
616        }
617    }
618
619    /// This method make `name` of [VarDeclarator] to [Pat::Invalid] if it
620    /// should be removed.
621    fn visit_mut_var_declarator(&mut self, d: &mut VarDeclarator) {
622        let old = self.in_lhs_of_var;
623        self.in_lhs_of_var = true;
624        d.name.visit_mut_with(self);
625
626        self.in_lhs_of_var = false;
627        if d.name.is_invalid() {
628            self.mark_as_candidate(&mut d.init);
629        }
630        d.init.visit_mut_with(self);
631        self.in_lhs_of_var = old;
632    }
633
634    fn visit_mut_var_declarators(&mut self, decls: &mut Vec<VarDeclarator>) {
635        decls.visit_mut_children_with(self);
636        decls.retain(|d| !d.name.is_invalid());
637    }
638}