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) if d.decls.is_empty() => {
251                    *s = ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }));
252                }
253                _ => {}
254            }
255        }
256    }
257
258    fn visit_mut_named_export(&mut self, n: &mut NamedExport) {
259        if n.src.is_some() {
260            n.specifiers.visit_mut_with(self);
261        }
262    }
263
264    fn visit_mut_prop(&mut self, p: &mut Prop) {
265        p.visit_mut_children_with(self);
266
267        if let Prop::Shorthand(i) = &p {
268            self.add_ref(i.to_id());
269        }
270    }
271
272    fn visit_mut_var_declarator(&mut self, v: &mut VarDeclarator) {
273        let old_in_data = self.in_data_fn;
274
275        if let Pat::Ident(name) = &v.name {
276            if let Ok(is_data_identifier) = self.state.is_data_identifier(&name.id) {
277                if is_data_identifier {
278                    self.in_data_fn = true;
279                }
280            } else {
281                return;
282            }
283        }
284
285        let old_in_lhs_of_var = self.in_lhs_of_var;
286
287        self.in_lhs_of_var = true;
288        v.name.visit_mut_with(self);
289
290        self.in_lhs_of_var = false;
291        v.init.visit_mut_with(self);
292
293        self.in_lhs_of_var = old_in_lhs_of_var;
294
295        self.in_data_fn = old_in_data;
296    }
297}
298
299/// Actual implementation of the transform.
300struct NextSsg {
301    pub state: State,
302    in_lhs_of_var: bool,
303}
304
305impl NextSsg {
306    fn should_remove(&self, id: Id) -> bool {
307        self.state.refs_from_data_fn.contains(&id) && !self.state.refs_from_other.contains(&id)
308    }
309
310    /// Mark identifiers in `n` as a candidate for removal.
311    fn mark_as_candidate<N>(&mut self, n: &mut N)
312    where
313        N: for<'aa> VisitMutWith<Analyzer<'aa>>,
314    {
315        tracing::debug!("mark_as_candidate");
316
317        // Analyzer never change `in_data_fn` to false, so all identifiers in `n` will
318        // be marked as referenced from a data function.
319        let mut v = Analyzer {
320            state: &mut self.state,
321            in_lhs_of_var: false,
322            in_data_fn: true,
323        };
324
325        n.visit_mut_with(&mut v);
326        self.state.should_run_again = true;
327    }
328}
329
330impl Repeated for NextSsg {
331    fn changed(&self) -> bool {
332        self.state.should_run_again
333    }
334
335    fn reset(&mut self) {
336        self.state.refs_from_other.clear();
337        self.state.cur_declaring.clear();
338        self.state.should_run_again = false;
339    }
340}
341
342/// Note: We don't implement `visit_mut_script` because next.js doesn't use it.
343impl VisitMut for NextSsg {
344    // This is important for reducing binary sizes.
345    noop_visit_mut_type!();
346
347    fn visit_mut_import_decl(&mut self, i: &mut ImportDecl) {
348        // Imports for side effects.
349        if i.specifiers.is_empty() {
350            return;
351        }
352
353        let import_src = &i.src.value;
354
355        i.specifiers.retain(|s| match s {
356            ImportSpecifier::Named(ImportNamedSpecifier { local, .. })
357            | ImportSpecifier::Default(ImportDefaultSpecifier { local, .. })
358            | ImportSpecifier::Namespace(ImportStarAsSpecifier { local, .. }) => {
359                if self.should_remove(local.to_id()) {
360                    if self.state.is_server_props
361                        // filter out non-packages import
362                        // third part packages must start with `a-z` or `@`
363                        && import_src.as_str().unwrap_or_default().starts_with(|c: char| c.is_ascii_lowercase() || c == '@')
364                    {
365                        self.state
366                            .eliminated_packages
367                            .borrow_mut()
368                            .insert(import_src.clone().to_atom_lossy().into_owned());
369                    }
370                    tracing::trace!(
371                        "Dropping import `{}{:?}` because it should be removed",
372                        local.sym,
373                        local.ctxt
374                    );
375
376                    self.state.should_run_again = true;
377                    false
378                } else {
379                    true
380                }
381            }
382        });
383    }
384
385    fn visit_mut_module(&mut self, m: &mut Module) {
386        tracing::info!("ssg: Start");
387        {
388            // Fill the state.
389            let mut v = Analyzer {
390                state: &mut self.state,
391                in_lhs_of_var: false,
392                in_data_fn: false,
393            };
394            m.visit_mut_with(&mut v);
395        }
396
397        // TODO: Use better detection logic
398        // if !self.state.is_prerenderer && !self.state.is_server_props {
399        //     return m;
400        // }
401
402        m.visit_mut_children_with(self)
403    }
404
405    fn visit_mut_module_item(&mut self, i: &mut ModuleItem) {
406        if let ModuleItem::ModuleDecl(ModuleDecl::Import(decl)) = i {
407            let is_for_side_effect = decl.specifiers.is_empty();
408            decl.visit_mut_with(self);
409
410            if !is_for_side_effect && decl.specifiers.is_empty() {
411                *i = ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }));
412                return;
413            }
414
415            return;
416        }
417
418        i.visit_mut_children_with(self);
419
420        match &i {
421            ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(e)) if e.specifiers.is_empty() => {
422                *i = ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP }));
423            }
424            _ => {}
425        }
426    }
427
428    fn visit_mut_module_items(&mut self, items: &mut Vec<ModuleItem>) {
429        items.visit_mut_children_with(self);
430
431        // Drop nodes.
432        items.retain(|s| !matches!(s, ModuleItem::Stmt(Stmt::Empty(..))));
433
434        if !self.state.done
435            && !self.state.should_run_again
436            && (self.state.is_prerenderer || self.state.is_server_props)
437        {
438            self.state.done = true;
439
440            if items.iter().any(|s| s.is_module_decl()) {
441                let mut var = Some(VarDeclarator {
442                    span: DUMMY_SP,
443                    name: Pat::Ident(
444                        IdentName::new(
445                            if self.state.is_prerenderer {
446                                atom!("__N_SSG")
447                            } else {
448                                atom!("__N_SSP")
449                            },
450                            DUMMY_SP,
451                        )
452                        .into(),
453                    ),
454                    init: Some(Box::new(Expr::Lit(Lit::Bool(Bool {
455                        span: DUMMY_SP,
456                        value: true,
457                    })))),
458                    definite: Default::default(),
459                });
460
461                let mut new = Vec::with_capacity(items.len() + 1);
462                for item in take(items) {
463                    if let ModuleItem::ModuleDecl(
464                        ModuleDecl::ExportNamed(..)
465                        | ModuleDecl::ExportDecl(..)
466                        | ModuleDecl::ExportDefaultDecl(..)
467                        | ModuleDecl::ExportDefaultExpr(..),
468                    ) = &item
469                        && let Some(var) = var.take()
470                    {
471                        new.push(ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
472                            span: DUMMY_SP,
473                            decl: Decl::Var(Box::new(VarDecl {
474                                span: DUMMY_SP,
475                                kind: VarDeclKind::Var,
476                                decls: vec![var],
477                                ..Default::default()
478                            })),
479                        })))
480                    }
481
482                    new.push(item);
483                }
484
485                *items = new;
486            }
487        }
488    }
489
490    fn visit_mut_named_export(&mut self, n: &mut NamedExport) {
491        n.specifiers.visit_mut_with(self);
492
493        n.specifiers.retain(|s| {
494            let preserve = match s {
495                ExportSpecifier::Namespace(ExportNamespaceSpecifier {
496                    name: ModuleExportName::Ident(exported),
497                    ..
498                })
499                | ExportSpecifier::Default(ExportDefaultSpecifier { exported, .. })
500                | ExportSpecifier::Named(ExportNamedSpecifier {
501                    exported: Some(ModuleExportName::Ident(exported)),
502                    ..
503                }) => self
504                    .state
505                    .is_data_identifier(exported)
506                    .map(|is_data_identifier| !is_data_identifier),
507                ExportSpecifier::Named(ExportNamedSpecifier {
508                    orig: ModuleExportName::Ident(orig),
509                    ..
510                }) => self
511                    .state
512                    .is_data_identifier(orig)
513                    .map(|is_data_identifier| !is_data_identifier),
514
515                _ => Ok(true),
516            };
517
518            match preserve {
519                Ok(false) => {
520                    tracing::trace!("Dropping a export specifier because it's a data identifier");
521
522                    if let ExportSpecifier::Named(ExportNamedSpecifier {
523                        orig: ModuleExportName::Ident(orig),
524                        ..
525                    }) = s
526                    {
527                        self.state.should_run_again = true;
528                        self.state.refs_from_data_fn.insert(orig.to_id());
529                    }
530
531                    false
532                }
533                Ok(true) => true,
534                Err(_) => false,
535            }
536        });
537    }
538
539    /// This methods returns [Pat::Invalid] if the pattern should be removed.
540    fn visit_mut_pat(&mut self, p: &mut Pat) {
541        p.visit_mut_children_with(self);
542
543        if self.in_lhs_of_var {
544            match p {
545                Pat::Ident(name) if self.should_remove(name.id.to_id()) => {
546                    self.state.should_run_again = true;
547                    tracing::trace!(
548                        "Dropping var `{}{:?}` because it should be removed",
549                        name.id.sym,
550                        name.id.ctxt
551                    );
552
553                    *p = Pat::Invalid(Invalid { span: DUMMY_SP });
554                }
555                Pat::Array(arr) if !arr.elems.is_empty() => {
556                    arr.elems.retain(|e| !matches!(e, Some(Pat::Invalid(..))));
557
558                    if arr.elems.is_empty() {
559                        *p = Pat::Invalid(Invalid { span: DUMMY_SP });
560                    }
561                }
562                Pat::Object(obj) if !obj.props.is_empty() => {
563                    obj.props.retain_mut(|prop| match prop {
564                        ObjectPatProp::KeyValue(prop) => !prop.value.is_invalid(),
565                        ObjectPatProp::Assign(prop) => {
566                            if self.should_remove(prop.key.to_id()) {
567                                self.mark_as_candidate(&mut prop.value);
568
569                                false
570                            } else {
571                                true
572                            }
573                        }
574                        ObjectPatProp::Rest(prop) => !prop.arg.is_invalid(),
575                    });
576
577                    if obj.props.is_empty() {
578                        *p = Pat::Invalid(Invalid { span: DUMMY_SP });
579                    }
580                }
581                Pat::Rest(rest) if rest.arg.is_invalid() => {
582                    *p = Pat::Invalid(Invalid { span: DUMMY_SP });
583                }
584                _ => {}
585            }
586        }
587    }
588
589    #[allow(clippy::single_match)]
590    fn visit_mut_stmt(&mut self, s: &mut Stmt) {
591        if let Stmt::Decl(Decl::Fn(f)) = s
592            && self.should_remove(f.ident.to_id())
593        {
594            self.mark_as_candidate(&mut f.function);
595            *s = Stmt::Empty(EmptyStmt { span: DUMMY_SP });
596            return;
597        }
598
599        s.visit_mut_children_with(self);
600        match s {
601            Stmt::Decl(Decl::Var(v)) if v.decls.is_empty() => {
602                *s = Stmt::Empty(EmptyStmt { span: DUMMY_SP });
603            }
604            _ => {}
605        }
606    }
607
608    /// This method make `name` of [VarDeclarator] to [Pat::Invalid] if it
609    /// should be removed.
610    fn visit_mut_var_declarator(&mut self, d: &mut VarDeclarator) {
611        let old = self.in_lhs_of_var;
612        self.in_lhs_of_var = true;
613        d.name.visit_mut_with(self);
614
615        self.in_lhs_of_var = false;
616        if d.name.is_invalid() {
617            self.mark_as_candidate(&mut d.init);
618        }
619        d.init.visit_mut_with(self);
620        self.in_lhs_of_var = old;
621    }
622
623    fn visit_mut_var_declarators(&mut self, decls: &mut Vec<VarDeclarator>) {
624        decls.visit_mut_children_with(self);
625        decls.retain(|d| !d.name.is_invalid());
626    }
627}