next_custom_transforms/transforms/
next_ssg.rs1use 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
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 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 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 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
301struct 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 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 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
344impl VisitMut for NextSsg {
346 noop_visit_mut_type!();
348
349 fn visit_mut_import_decl(&mut self, i: &mut ImportDecl) {
350 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 && 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 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 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 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 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 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}