next_custom_transforms/transforms/
next_ssg.rs

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