1use std::{
2 path::{Path, PathBuf},
3 sync::Arc,
4};
5
6use pathdiff::diff_paths;
7use swc_core::{
8 atoms::Atom,
9 common::{errors::HANDLER, FileName, Span, DUMMY_SP},
10 ecma::{
11 ast::{
12 op, ArrayLit, ArrowExpr, BinExpr, BlockStmt, BlockStmtOrExpr, Bool, CallExpr, Callee,
13 Expr, ExprOrSpread, ExprStmt, Id, Ident, IdentName, ImportDecl, ImportNamedSpecifier,
14 ImportSpecifier, KeyValueProp, Lit, ModuleDecl, ModuleItem, ObjectLit, Pass, Prop,
15 PropName, PropOrSpread, Stmt, Str, Tpl, UnaryExpr, UnaryOp,
16 },
17 utils::{private_ident, quote_ident, ExprFactory},
18 visit::{fold_pass, Fold, FoldWith, VisitMut, VisitMutWith},
19 },
20 quote,
21};
22
23pub fn next_dynamic(
29 is_development: bool,
30 is_server_compiler: bool,
31 is_react_server_layer: bool,
32 prefer_esm: bool,
33 mode: NextDynamicMode,
34 filename: Arc<FileName>,
35 pages_or_app_dir: Option<PathBuf>,
36) -> impl Pass {
37 fold_pass(NextDynamicPatcher {
38 is_development,
39 is_server_compiler,
40 is_react_server_layer,
41 prefer_esm,
42 pages_or_app_dir,
43 filename,
44 dynamic_bindings: vec![],
45 is_next_dynamic_first_arg: false,
46 dynamically_imported_specifier: None,
47 state: match mode {
48 NextDynamicMode::Webpack => NextDynamicPatcherState::Webpack,
49 NextDynamicMode::Turbopack {
50 dynamic_client_transition_name,
51 dynamic_transition_name,
52 } => NextDynamicPatcherState::Turbopack {
53 dynamic_client_transition_name,
54 dynamic_transition_name,
55 imports: vec![],
56 },
57 },
58 })
59}
60
61#[derive(Debug, Clone, Eq, PartialEq)]
62pub enum NextDynamicMode {
63 Webpack,
74 Turbopack {
79 dynamic_client_transition_name: Atom,
80 dynamic_transition_name: Atom,
81 },
82}
83
84#[derive(Debug)]
85struct NextDynamicPatcher {
86 is_development: bool,
87 is_server_compiler: bool,
88 is_react_server_layer: bool,
89 prefer_esm: bool,
90 pages_or_app_dir: Option<PathBuf>,
91 filename: Arc<FileName>,
92 dynamic_bindings: Vec<Id>,
93 is_next_dynamic_first_arg: bool,
94 dynamically_imported_specifier: Option<(Atom, Span)>,
95 state: NextDynamicPatcherState,
96}
97
98#[derive(Debug, Clone, Eq, PartialEq)]
99enum NextDynamicPatcherState {
100 Webpack,
101 #[allow(unused)]
104 Turbopack {
105 dynamic_client_transition_name: Atom,
106 dynamic_transition_name: Atom,
107 imports: Vec<TurbopackImport>,
108 },
109}
110
111#[derive(Debug, Clone, Eq, PartialEq)]
112enum TurbopackImport {
113 Import { id_ident: Ident, specifier: Atom },
115}
116
117impl Fold for NextDynamicPatcher {
118 fn fold_module_items(&mut self, mut items: Vec<ModuleItem>) -> Vec<ModuleItem> {
119 items = items.fold_children_with(self);
120
121 self.maybe_add_dynamically_imported_specifier(&mut items);
122
123 items
124 }
125
126 fn fold_import_decl(&mut self, decl: ImportDecl) -> ImportDecl {
127 let ImportDecl {
128 ref src,
129 ref specifiers,
130 ..
131 } = decl;
132 if &src.value == "next/dynamic" {
133 for specifier in specifiers {
134 if let ImportSpecifier::Default(default_specifier) = specifier {
135 self.dynamic_bindings.push(default_specifier.local.to_id());
136 }
137 }
138 }
139
140 decl
141 }
142
143 fn fold_call_expr(&mut self, expr: CallExpr) -> CallExpr {
144 if self.is_next_dynamic_first_arg {
145 if let Callee::Import(..) = &expr.callee {
146 match &*expr.args[0].expr {
147 Expr::Lit(Lit::Str(Str { value, span, .. })) => {
148 self.dynamically_imported_specifier = Some((value.clone(), *span));
149 }
150 Expr::Tpl(Tpl { exprs, quasis, .. }) if exprs.is_empty() => {
151 self.dynamically_imported_specifier =
152 Some((quasis[0].raw.clone(), quasis[0].span));
153 }
154 _ => {}
155 }
156 }
157 return expr.fold_children_with(self);
158 }
159 let mut expr = expr.fold_children_with(self);
160 if let Callee::Expr(i) = &expr.callee {
161 if let Expr::Ident(identifier) = &**i {
162 if self.dynamic_bindings.contains(&identifier.to_id()) {
163 if expr.args.is_empty() {
164 HANDLER.with(|handler| {
165 handler
166 .struct_span_err(
167 identifier.span,
168 "next/dynamic requires at least one argument",
169 )
170 .emit()
171 });
172 return expr;
173 } else if expr.args.len() > 2 {
174 HANDLER.with(|handler| {
175 handler
176 .struct_span_err(
177 identifier.span,
178 "next/dynamic only accepts 2 arguments",
179 )
180 .emit()
181 });
182 return expr;
183 }
184 if expr.args.len() == 2 {
185 match &*expr.args[1].expr {
186 Expr::Object(_) => {}
187 _ => {
188 HANDLER.with(|handler| {
189 handler
190 .struct_span_err(
191 identifier.span,
192 "next/dynamic options must be an object literal.\nRead more: https://nextjs.org/docs/messages/invalid-dynamic-options-type",
193 )
194 .emit();
195 });
196 return expr;
197 }
198 }
199 }
200
201 self.is_next_dynamic_first_arg = true;
202 expr.args[0].expr = expr.args[0].expr.clone().fold_with(self);
203 self.is_next_dynamic_first_arg = false;
204
205 let Some((dynamically_imported_specifier, dynamically_imported_specifier_span)) =
206 self.dynamically_imported_specifier.take()
207 else {
208 return expr;
209 };
210
211 let project_dir = match self.pages_or_app_dir.as_deref() {
212 Some(pages_or_app) => pages_or_app.parent(),
213 _ => None,
214 };
215
216 let generated = Box::new(Expr::Object(ObjectLit {
217 span: DUMMY_SP,
218 props: match &mut self.state {
219 NextDynamicPatcherState::Webpack => {
220 if self.is_development || self.is_server_compiler {
230 module_id_options(quote!(
231 "$left + $right" as Expr,
232 left: Expr = format!(
233 "{} -> ",
234 rel_filename(project_dir, &self.filename)
235 )
236 .into(),
237 right: Expr = dynamically_imported_specifier.clone().into(),
238 ))
239 } else {
240 webpack_options(quote!(
241 "require.resolveWeak($id)" as Expr,
242 id: Expr = dynamically_imported_specifier.clone().into()
243 ))
244 }
245 }
246
247 NextDynamicPatcherState::Turbopack { imports, .. } => {
248 let id_ident =
252 private_ident!(dynamically_imported_specifier_span, "id");
253
254 imports.push(TurbopackImport::Import {
255 id_ident: id_ident.clone(),
256 specifier: dynamically_imported_specifier.clone(),
257 });
258
259 module_id_options(Expr::Ident(id_ident))
260 }
261 },
262 }));
263
264 let mut props =
265 vec![PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
266 key: PropName::Ident(IdentName::new(
267 "loadableGenerated".into(),
268 DUMMY_SP,
269 )),
270 value: generated,
271 })))];
272
273 let mut has_ssr_false = false;
274
275 if expr.args.len() == 2 {
276 if let Expr::Object(ObjectLit {
277 props: options_props,
278 ..
279 }) = &*expr.args[1].expr
280 {
281 for prop in options_props.iter() {
282 if let Some(KeyValueProp { key, value }) = match prop {
283 PropOrSpread::Prop(prop) => match &**prop {
284 Prop::KeyValue(key_value_prop) => Some(key_value_prop),
285 _ => None,
286 },
287 _ => None,
288 } {
289 if let Some(IdentName { sym, span: _ }) = match key {
290 PropName::Ident(ident) => Some(ident),
291 _ => None,
292 } {
293 if sym == "ssr" {
294 if let Some(Lit::Bool(Bool {
295 value: false,
296 span: _,
297 })) = value.as_lit()
298 {
299 has_ssr_false = true
300 }
301 }
302 }
303 }
304 }
305 props.extend(options_props.iter().cloned());
306 }
307 }
308
309 let should_skip_ssr_compile = has_ssr_false
310 && self.is_server_compiler
311 && !self.is_react_server_layer
312 && self.prefer_esm;
313
314 match &self.state {
315 NextDynamicPatcherState::Webpack => {
316 if should_skip_ssr_compile {
325 let require_resolve_weak_expr = Expr::Call(CallExpr {
334 span: DUMMY_SP,
335 callee: quote_ident!("require.resolveWeak").as_callee(),
336 args: vec![ExprOrSpread {
337 spread: None,
338 expr: Box::new(Expr::Lit(Lit::Str(Str {
339 span: DUMMY_SP,
340 value: dynamically_imported_specifier.clone(),
341 raw: None,
342 }))),
343 }],
344 ..Default::default()
345 });
346
347 let side_effect_free_loader_arg = Expr::Arrow(ArrowExpr {
348 span: DUMMY_SP,
349 params: vec![],
350 body: Box::new(BlockStmtOrExpr::BlockStmt(BlockStmt {
351 span: DUMMY_SP,
352 stmts: vec![Stmt::Expr(ExprStmt {
353 span: DUMMY_SP,
354 expr: Box::new(exec_expr_when_resolve_weak_available(
355 &require_resolve_weak_expr,
356 )),
357 })],
358 ..Default::default()
359 })),
360 is_async: true,
361 is_generator: false,
362 ..Default::default()
363 });
364
365 expr.args[0] = side_effect_free_loader_arg.as_arg();
366 }
367 }
368 NextDynamicPatcherState::Turbopack {
369 dynamic_transition_name,
370 ..
371 } => {
372 if should_skip_ssr_compile {
379 let side_effect_free_loader_arg = Expr::Arrow(ArrowExpr {
380 span: DUMMY_SP,
381 params: vec![],
382 body: Box::new(BlockStmtOrExpr::BlockStmt(BlockStmt {
383 span: DUMMY_SP,
384 stmts: vec![],
385 ..Default::default()
386 })),
387 is_async: true,
388 is_generator: false,
389 ..Default::default()
390 });
391
392 expr.args[0] = side_effect_free_loader_arg.as_arg();
393 } else {
394 let mut visitor = DynamicImportTransitionAdder {
396 transition_name: dynamic_transition_name,
397 };
398 expr.args[0].visit_mut_with(&mut visitor);
399 }
400 }
401 }
402
403 let second_arg = ExprOrSpread {
404 spread: None,
405 expr: Box::new(Expr::Object(ObjectLit {
406 span: DUMMY_SP,
407 props,
408 })),
409 };
410
411 if expr.args.len() == 2 {
412 expr.args[1] = second_arg;
413 } else {
414 expr.args.push(second_arg)
415 }
416 }
417 }
418 }
419 expr
420 }
421}
422
423struct DynamicImportTransitionAdder<'a> {
424 transition_name: &'a str,
425}
426impl VisitMut for DynamicImportTransitionAdder<'_> {
428 fn visit_mut_call_expr(&mut self, expr: &mut CallExpr) {
429 if let Callee::Import(..) = &expr.callee {
430 let options = ExprOrSpread {
431 expr: Box::new(
432 ObjectLit {
433 span: DUMMY_SP,
434 props: vec![PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
435 key: PropName::Ident(IdentName::new("with".into(), DUMMY_SP)),
436 value: with_transition(self.transition_name).into(),
437 })))],
438 }
439 .into(),
440 ),
441 spread: None,
442 };
443
444 match expr.args.get_mut(1) {
445 Some(arg) => *arg = options,
446 None => expr.args.push(options),
447 }
448 } else {
449 expr.visit_mut_children_with(self);
450 }
451 }
452}
453
454fn module_id_options(module_id: Expr) -> Vec<PropOrSpread> {
455 vec![PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
456 key: PropName::Ident(IdentName::new("modules".into(), DUMMY_SP)),
457 value: Box::new(Expr::Array(ArrayLit {
458 elems: vec![Some(ExprOrSpread {
459 expr: Box::new(module_id),
460 spread: None,
461 })],
462 span: DUMMY_SP,
463 })),
464 })))]
465}
466
467fn webpack_options(module_id: Expr) -> Vec<PropOrSpread> {
468 vec![PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
469 key: PropName::Ident(IdentName::new("webpack".into(), DUMMY_SP)),
470 value: Box::new(Expr::Arrow(ArrowExpr {
471 params: vec![],
472 body: Box::new(BlockStmtOrExpr::Expr(Box::new(Expr::Array(ArrayLit {
473 elems: vec![Some(ExprOrSpread {
474 expr: Box::new(module_id),
475 spread: None,
476 })],
477 span: DUMMY_SP,
478 })))),
479 is_async: false,
480 is_generator: false,
481 span: DUMMY_SP,
482 ..Default::default()
483 })),
484 })))]
485}
486
487impl NextDynamicPatcher {
488 fn maybe_add_dynamically_imported_specifier(&mut self, items: &mut Vec<ModuleItem>) {
489 let NextDynamicPatcherState::Turbopack {
490 dynamic_client_transition_name,
491 imports,
492 ..
493 } = &mut self.state
494 else {
495 return;
496 };
497
498 let mut new_items = Vec::with_capacity(imports.len());
499
500 for import in std::mem::take(imports) {
501 match import {
502 TurbopackImport::Import {
503 id_ident,
504 specifier,
505 } => {
506 new_items.push(ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
509 span: DUMMY_SP,
510 specifiers: vec![ImportSpecifier::Named(ImportNamedSpecifier {
511 span: DUMMY_SP,
512 local: id_ident,
513 imported: Some(
514 Ident::new(
515 "__turbopack_module_id__".into(),
516 DUMMY_SP,
517 Default::default(),
518 )
519 .into(),
520 ),
521 is_type_only: false,
522 })],
523 src: Box::new(specifier.into()),
524 type_only: false,
525 with: Some(with_transition_chunking_type(
526 dynamic_client_transition_name,
527 "none",
528 )),
529 phase: Default::default(),
530 })));
531 }
532 }
533 }
534
535 new_items.append(items);
536
537 std::mem::swap(&mut new_items, items)
538 }
539}
540
541fn exec_expr_when_resolve_weak_available(expr: &Expr) -> Expr {
542 let undefined_str_literal = Expr::Lit(Lit::Str(Str {
543 span: DUMMY_SP,
544 value: "undefined".into(),
545 raw: None,
546 }));
547
548 let typeof_expr = Expr::Unary(UnaryExpr {
549 span: DUMMY_SP,
550 op: UnaryOp::TypeOf, arg: Box::new(Expr::Ident(Ident {
552 sym: quote_ident!("require.resolveWeak").sym,
553 ..Default::default()
554 })),
555 });
556
557 Expr::Bin(BinExpr {
559 span: DUMMY_SP,
560 left: Box::new(Expr::Bin(BinExpr {
561 span: DUMMY_SP,
562 op: op!("!=="),
563 left: Box::new(typeof_expr),
564 right: Box::new(undefined_str_literal),
565 })),
566 op: op!("&&"),
567 right: Box::new(expr.clone()),
568 })
569}
570
571fn rel_filename(base: Option<&Path>, file: &FileName) -> String {
572 let base = match base {
573 Some(v) => v,
574 None => return file.to_string(),
575 };
576
577 let file = match file {
578 FileName::Real(v) => v,
579 _ => {
580 return file.to_string();
581 }
582 };
583
584 let rel_path = diff_paths(file, base);
585
586 let rel_path = match rel_path {
587 Some(v) => v,
588 None => return file.display().to_string(),
589 };
590
591 rel_path.display().to_string()
592}
593
594fn with_transition(transition_name: &str) -> ObjectLit {
595 with_clause(&[("turbopack-transition", transition_name)])
596}
597
598fn with_transition_chunking_type(transition_name: &str, chunking_type: &str) -> Box<ObjectLit> {
599 Box::new(with_clause(&[
600 ("turbopack-transition", transition_name),
601 ("turbopack-chunking-type", chunking_type),
602 ]))
603}
604
605fn with_clause<'a>(entries: impl IntoIterator<Item = &'a (&'a str, &'a str)>) -> ObjectLit {
606 ObjectLit {
607 span: DUMMY_SP,
608 props: entries.into_iter().map(|(k, v)| with_prop(k, v)).collect(),
609 }
610}
611
612fn with_prop(key: &str, value: &str) -> PropOrSpread {
613 PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
614 key: PropName::Str(key.into()),
615 value: Box::new(Expr::Lit(value.into())),
616 })))
617}