next_custom_transforms/transforms/
next_ssg.rs1use 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
20pub 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#[derive(Debug, Default)]
33struct State {
34 refs_from_other: FxHashSet<Id>,
39
40 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 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 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 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 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 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
299struct 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 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 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
342impl VisitMut for NextSsg {
344 noop_visit_mut_type!();
346
347 fn visit_mut_import_decl(&mut self, i: &mut ImportDecl) {
348 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 && 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 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 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 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 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 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}