Skip to main content

next_custom_transforms/transforms/
next_ssg.rs

1use std::{cell::RefCell, mem::take, rc::Rc};
2
3use easy_error::{Error, bail};
4use rustc_hash::FxHashSet;
5use swc_core::{
6    atoms::{Atom, atom},
7    common::{
8        DUMMY_SP,
9        errors::HANDLER,
10        pass::{Repeat, Repeated},
11    },
12    ecma::{
13        ast::*,
14        visit::{VisitMut, VisitMutWith, noop_visit_mut_type, visit_mut_pass},
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            && !SSG_EXPORTS.contains(&&*id.sym)
136        {
137            self.add_ref(id.to_id());
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                    && !SSG_EXPORTS.contains(&&*id.id.sym)
150                {
151                    self.add_ref(id.to_id());
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                        && is_data_identifier
245                    {
246                        *s = ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }));
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                        && let Some(var) = var.take()
472                    {
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                    new.push(item);
485                }
486
487                *items = new;
488            }
489        }
490    }
491
492    fn visit_mut_named_export(&mut self, n: &mut NamedExport) {
493        n.specifiers.visit_mut_with(self);
494
495        n.specifiers.retain(|s| {
496            let preserve = match s {
497                ExportSpecifier::Namespace(ExportNamespaceSpecifier {
498                    name: ModuleExportName::Ident(exported),
499                    ..
500                })
501                | ExportSpecifier::Default(ExportDefaultSpecifier { exported, .. })
502                | ExportSpecifier::Named(ExportNamedSpecifier {
503                    exported: Some(ModuleExportName::Ident(exported)),
504                    ..
505                }) => self
506                    .state
507                    .is_data_identifier(exported)
508                    .map(|is_data_identifier| !is_data_identifier),
509                ExportSpecifier::Named(ExportNamedSpecifier {
510                    orig: ModuleExportName::Ident(orig),
511                    ..
512                }) => self
513                    .state
514                    .is_data_identifier(orig)
515                    .map(|is_data_identifier| !is_data_identifier),
516
517                _ => Ok(true),
518            };
519
520            match preserve {
521                Ok(false) => {
522                    tracing::trace!("Dropping a export specifier because it's a data identifier");
523
524                    if let ExportSpecifier::Named(ExportNamedSpecifier {
525                        orig: ModuleExportName::Ident(orig),
526                        ..
527                    }) = s
528                    {
529                        self.state.should_run_again = true;
530                        self.state.refs_from_data_fn.insert(orig.to_id());
531                    }
532
533                    false
534                }
535                Ok(true) => true,
536                Err(_) => false,
537            }
538        });
539    }
540
541    /// This methods returns [Pat::Invalid] if the pattern should be removed.
542    fn visit_mut_pat(&mut self, p: &mut Pat) {
543        p.visit_mut_children_with(self);
544
545        if self.in_lhs_of_var {
546            match p {
547                Pat::Ident(name) => {
548                    if self.should_remove(name.id.to_id()) {
549                        self.state.should_run_again = true;
550                        tracing::trace!(
551                            "Dropping var `{}{:?}` because it should be removed",
552                            name.id.sym,
553                            name.id.ctxt
554                        );
555
556                        *p = Pat::Invalid(Invalid { span: DUMMY_SP });
557                    }
558                }
559                Pat::Array(arr) => {
560                    if !arr.elems.is_empty() {
561                        arr.elems.retain(|e| !matches!(e, Some(Pat::Invalid(..))));
562
563                        if arr.elems.is_empty() {
564                            *p = Pat::Invalid(Invalid { span: DUMMY_SP });
565                        }
566                    }
567                }
568                Pat::Object(obj) => {
569                    if !obj.props.is_empty() {
570                        obj.props.retain_mut(|prop| match prop {
571                            ObjectPatProp::KeyValue(prop) => !prop.value.is_invalid(),
572                            ObjectPatProp::Assign(prop) => {
573                                if self.should_remove(prop.key.to_id()) {
574                                    self.mark_as_candidate(&mut prop.value);
575
576                                    false
577                                } else {
578                                    true
579                                }
580                            }
581                            ObjectPatProp::Rest(prop) => !prop.arg.is_invalid(),
582                        });
583
584                        if obj.props.is_empty() {
585                            *p = Pat::Invalid(Invalid { span: DUMMY_SP });
586                        }
587                    }
588                }
589                Pat::Rest(rest) => {
590                    if rest.arg.is_invalid() {
591                        *p = Pat::Invalid(Invalid { span: DUMMY_SP });
592                    }
593                }
594                _ => {}
595            }
596        }
597    }
598
599    #[allow(clippy::single_match)]
600    fn visit_mut_stmt(&mut self, s: &mut Stmt) {
601        if let Stmt::Decl(Decl::Fn(f)) = s
602            && self.should_remove(f.ident.to_id())
603        {
604            self.mark_as_candidate(&mut f.function);
605            *s = Stmt::Empty(EmptyStmt { span: DUMMY_SP });
606            return;
607        }
608
609        s.visit_mut_children_with(self);
610        match s {
611            Stmt::Decl(Decl::Var(v)) if v.decls.is_empty() => {
612                *s = Stmt::Empty(EmptyStmt { span: DUMMY_SP });
613            }
614            _ => {}
615        }
616    }
617
618    /// This method make `name` of [VarDeclarator] to [Pat::Invalid] if it
619    /// should be removed.
620    fn visit_mut_var_declarator(&mut self, d: &mut VarDeclarator) {
621        let old = self.in_lhs_of_var;
622        self.in_lhs_of_var = true;
623        d.name.visit_mut_with(self);
624
625        self.in_lhs_of_var = false;
626        if d.name.is_invalid() {
627            self.mark_as_candidate(&mut d.init);
628        }
629        d.init.visit_mut_with(self);
630        self.in_lhs_of_var = old;
631    }
632
633    fn visit_mut_var_declarators(&mut self, decls: &mut Vec<VarDeclarator>) {
634        decls.visit_mut_children_with(self);
635        decls.retain(|d| !d.name.is_invalid());
636    }
637}