1use std::{
2 path::{Path, PathBuf},
3 sync::Arc,
4};
5
6use pathdiff::diff_paths;
7use swc_core::{
8 atoms::{Atom, Wtf8Atom, atom},
9 common::{DUMMY_SP, FileName, Span, errors::HANDLER},
10 ecma::{
11 ast::{
12 ArrayLit, ArrowExpr, BinExpr, BlockStmt, BlockStmtOrExpr, Bool, CallExpr, Callee, Expr,
13 ExprOrSpread, ExprStmt, Id, Ident, IdentName, ImportDecl, ImportNamedSpecifier,
14 ImportSpecifier, KeyValueProp, Lit, ModuleDecl, ModuleItem, ObjectLit, Pass, Prop,
15 PropName, PropOrSpread, Stmt, Str, Tpl, UnaryExpr, UnaryOp, op,
16 },
17 utils::{ExprFactory, private_ident, quote_ident},
18 visit::{VisitMut, VisitMutWith, visit_mut_pass},
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 visit_mut_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<(Wtf8Atom, 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 {
115 id_ident: Ident,
116 specifier: Wtf8Atom,
117 },
118}
119
120impl VisitMut for NextDynamicPatcher {
121 fn visit_mut_module_items(&mut self, items: &mut Vec<ModuleItem>) {
122 items.visit_mut_children_with(self);
123
124 self.maybe_add_dynamically_imported_specifier(items);
125 }
126
127 fn visit_mut_import_decl(&mut self, decl: &mut ImportDecl) {
128 if &decl.src.value == "next/dynamic" {
129 for specifier in &decl.specifiers {
130 if let ImportSpecifier::Default(default_specifier) = specifier {
131 self.dynamic_bindings.push(default_specifier.local.to_id());
132 }
133 }
134 }
135 }
136
137 fn visit_mut_call_expr(&mut self, expr: &mut CallExpr) {
138 if self.is_next_dynamic_first_arg {
139 if let Callee::Import(..) = &expr.callee {
140 match &*expr.args[0].expr {
141 Expr::Lit(Lit::Str(Str { value, span, .. })) => {
142 self.dynamically_imported_specifier = Some((value.clone(), *span));
143 }
144 Expr::Tpl(Tpl { exprs, quasis, .. }) if exprs.is_empty() => {
145 self.dynamically_imported_specifier =
146 Some((quasis[0].raw.clone().into(), quasis[0].span));
147 }
148 _ => {}
149 }
150 }
151 expr.visit_mut_children_with(self);
152 return;
153 }
154
155 expr.visit_mut_children_with(self);
156
157 if let Callee::Expr(i) = &expr.callee
158 && let Expr::Ident(identifier) = &**i
159 && self.dynamic_bindings.contains(&identifier.to_id())
160 {
161 if expr.args.is_empty() {
162 HANDLER.with(|handler| {
163 handler
164 .struct_span_err(
165 identifier.span,
166 "next/dynamic requires at least one argument",
167 )
168 .emit()
169 });
170 return;
171 } else if expr.args.len() > 2 {
172 HANDLER.with(|handler| {
173 handler
174 .struct_span_err(identifier.span, "next/dynamic only accepts 2 arguments")
175 .emit()
176 });
177 return;
178 }
179 if expr.args.len() == 2 {
180 match &*expr.args[1].expr {
181 Expr::Object(_) => {}
182 _ => {
183 HANDLER.with(|handler| {
184 handler
185 .struct_span_err(
186 identifier.span,
187 "next/dynamic options must be an object literal.\nRead more: https://nextjs.org/docs/messages/invalid-dynamic-options-type",
188 )
189 .emit();
190 });
191 return;
192 }
193 }
194 }
195
196 self.is_next_dynamic_first_arg = true;
197 expr.args[0].expr.visit_mut_with(self);
198 self.is_next_dynamic_first_arg = false;
199
200 let Some((dynamically_imported_specifier, dynamically_imported_specifier_span)) =
201 self.dynamically_imported_specifier.take()
202 else {
203 return;
204 };
205
206 let project_dir = match self.pages_or_app_dir.as_deref() {
207 Some(pages_or_app) => pages_or_app.parent(),
208 _ => None,
209 };
210
211 let generated = Box::new(Expr::Object(ObjectLit {
212 span: DUMMY_SP,
213 props: match &mut self.state {
214 NextDynamicPatcherState::Webpack => {
215 if self.is_development || self.is_server_compiler {
225 module_id_options(quote!(
226 "$left + $right" as Expr,
227 left: Expr = format!(
228 "{} -> ",
229 rel_filename(project_dir, &self.filename)
230 )
231 .into(),
232 right: Expr = dynamically_imported_specifier.clone().into(),
233 ))
234 } else {
235 webpack_options(quote!(
236 "require.resolveWeak($id)" as Expr,
237 id: Expr = dynamically_imported_specifier.clone().into()
238 ))
239 }
240 }
241
242 NextDynamicPatcherState::Turbopack { imports, .. } => {
243 let id_ident = private_ident!(dynamically_imported_specifier_span, "id");
247
248 imports.push(TurbopackImport::Import {
249 id_ident: id_ident.clone(),
250 specifier: dynamically_imported_specifier.clone(),
251 });
252
253 module_id_options(Expr::Ident(id_ident))
254 }
255 },
256 }));
257
258 let mut props = vec![PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
259 key: PropName::Ident(IdentName::new(atom!("loadableGenerated"), DUMMY_SP)),
260 value: generated,
261 })))];
262
263 let mut has_ssr_false = false;
264
265 if expr.args.len() == 2
266 && let Expr::Object(ObjectLit {
267 props: options_props,
268 ..
269 }) = &*expr.args[1].expr
270 {
271 for prop in options_props.iter() {
272 if let Some(KeyValueProp { key, value }) = match prop {
273 PropOrSpread::Prop(prop) => match &**prop {
274 Prop::KeyValue(key_value_prop) => Some(key_value_prop),
275 _ => None,
276 },
277 _ => None,
278 } && let Some(IdentName { sym, span: _ }) = match key {
279 PropName::Ident(ident) => Some(ident),
280 _ => None,
281 } && sym == "ssr"
282 && let Some(Lit::Bool(Bool {
283 value: false,
284 span: _,
285 })) = value.as_lit()
286 {
287 has_ssr_false = true
288 }
289 }
290 props.extend(options_props.iter().cloned());
291 }
292
293 let should_skip_ssr_compile = has_ssr_false
294 && self.is_server_compiler
295 && !self.is_react_server_layer
296 && self.prefer_esm;
297
298 match &self.state {
299 NextDynamicPatcherState::Webpack => {
300 if should_skip_ssr_compile {
309 let require_resolve_weak_expr = Expr::Call(CallExpr {
318 span: DUMMY_SP,
319 callee: quote_ident!("require.resolveWeak").as_callee(),
320 args: vec![ExprOrSpread {
321 spread: None,
322 expr: Box::new(Expr::Lit(Lit::Str(Str {
323 span: DUMMY_SP,
324 value: dynamically_imported_specifier.clone(),
325 raw: None,
326 }))),
327 }],
328 ..Default::default()
329 });
330
331 let side_effect_free_loader_arg = Expr::Arrow(ArrowExpr {
332 span: DUMMY_SP,
333 params: vec![],
334 body: Box::new(BlockStmtOrExpr::BlockStmt(BlockStmt {
335 span: DUMMY_SP,
336 stmts: vec![Stmt::Expr(ExprStmt {
337 span: DUMMY_SP,
338 expr: Box::new(exec_expr_when_resolve_weak_available(
339 &require_resolve_weak_expr,
340 )),
341 })],
342 ..Default::default()
343 })),
344 is_async: true,
345 is_generator: false,
346 ..Default::default()
347 });
348
349 expr.args[0] = side_effect_free_loader_arg.as_arg();
350 }
351 }
352 NextDynamicPatcherState::Turbopack {
353 dynamic_transition_name,
354 ..
355 } => {
356 if should_skip_ssr_compile {
363 let side_effect_free_loader_arg = Expr::Arrow(ArrowExpr {
364 span: DUMMY_SP,
365 params: vec![],
366 body: Box::new(BlockStmtOrExpr::BlockStmt(BlockStmt {
367 span: DUMMY_SP,
368 stmts: vec![],
369 ..Default::default()
370 })),
371 is_async: true,
372 is_generator: false,
373 ..Default::default()
374 });
375
376 expr.args[0] = side_effect_free_loader_arg.as_arg();
377 } else {
378 let mut visitor = DynamicImportTransitionAdder {
380 transition_name: dynamic_transition_name,
381 };
382 expr.args[0].visit_mut_with(&mut visitor);
383 }
384 }
385 }
386
387 let second_arg = ExprOrSpread {
388 spread: None,
389 expr: Box::new(Expr::Object(ObjectLit {
390 span: DUMMY_SP,
391 props,
392 })),
393 };
394
395 if expr.args.len() == 2 {
396 expr.args[1] = second_arg;
397 } else {
398 expr.args.push(second_arg)
399 }
400 }
401 }
402}
403
404struct DynamicImportTransitionAdder<'a> {
405 transition_name: &'a str,
406}
407impl VisitMut for DynamicImportTransitionAdder<'_> {
409 fn visit_mut_call_expr(&mut self, expr: &mut CallExpr) {
410 if let Callee::Import(..) = &expr.callee {
411 let options = ExprOrSpread {
412 expr: Box::new(
413 ObjectLit {
414 span: DUMMY_SP,
415 props: vec![PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
416 key: PropName::Ident(IdentName::new(atom!("with"), DUMMY_SP)),
417 value: with_transition(self.transition_name).into(),
418 })))],
419 }
420 .into(),
421 ),
422 spread: None,
423 };
424
425 match expr.args.get_mut(1) {
426 Some(arg) => *arg = options,
427 None => expr.args.push(options),
428 }
429 } else {
430 expr.visit_mut_children_with(self);
431 }
432 }
433}
434
435fn module_id_options(module_id: Expr) -> Vec<PropOrSpread> {
436 vec![PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
437 key: PropName::Ident(IdentName::new(atom!("modules"), DUMMY_SP)),
438 value: Box::new(Expr::Array(ArrayLit {
439 elems: vec![Some(ExprOrSpread {
440 expr: Box::new(module_id),
441 spread: None,
442 })],
443 span: DUMMY_SP,
444 })),
445 })))]
446}
447
448fn webpack_options(module_id: Expr) -> Vec<PropOrSpread> {
449 vec![PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
450 key: PropName::Ident(IdentName::new(atom!("webpack"), DUMMY_SP)),
451 value: Box::new(Expr::Arrow(ArrowExpr {
452 params: vec![],
453 body: Box::new(BlockStmtOrExpr::Expr(Box::new(Expr::Array(ArrayLit {
454 elems: vec![Some(ExprOrSpread {
455 expr: Box::new(module_id),
456 spread: None,
457 })],
458 span: DUMMY_SP,
459 })))),
460 is_async: false,
461 is_generator: false,
462 span: DUMMY_SP,
463 ..Default::default()
464 })),
465 })))]
466}
467
468impl NextDynamicPatcher {
469 fn maybe_add_dynamically_imported_specifier(&mut self, items: &mut Vec<ModuleItem>) {
470 let NextDynamicPatcherState::Turbopack {
471 dynamic_client_transition_name,
472 imports,
473 ..
474 } = &mut self.state
475 else {
476 return;
477 };
478
479 let mut new_items = Vec::with_capacity(imports.len());
480
481 for import in std::mem::take(imports) {
482 match import {
483 TurbopackImport::Import {
484 id_ident,
485 specifier,
486 } => {
487 new_items.push(ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
490 span: DUMMY_SP,
491 specifiers: vec![ImportSpecifier::Named(ImportNamedSpecifier {
492 span: DUMMY_SP,
493 local: id_ident,
494 imported: Some(
495 Ident::new(
496 atom!("__turbopack_module_id__"),
497 DUMMY_SP,
498 Default::default(),
499 )
500 .into(),
501 ),
502 is_type_only: false,
503 })],
504 src: Box::new(specifier.into()),
505 type_only: false,
506 with: Some(with_transition_chunking_type(
507 dynamic_client_transition_name,
508 "none",
509 )),
510 phase: Default::default(),
511 })));
512 }
513 }
514 }
515
516 new_items.append(items);
517
518 std::mem::swap(&mut new_items, items)
519 }
520}
521
522fn exec_expr_when_resolve_weak_available(expr: &Expr) -> Expr {
523 let undefined_str_literal = Expr::Lit(Lit::Str(Str {
524 span: DUMMY_SP,
525 value: atom!("undefined").into(),
526 raw: None,
527 }));
528
529 let typeof_expr = Expr::Unary(UnaryExpr {
530 span: DUMMY_SP,
531 op: UnaryOp::TypeOf, arg: Box::new(Expr::Ident(Ident {
533 sym: quote_ident!("require.resolveWeak").sym,
534 ..Default::default()
535 })),
536 });
537
538 Expr::Bin(BinExpr {
540 span: DUMMY_SP,
541 left: Box::new(Expr::Bin(BinExpr {
542 span: DUMMY_SP,
543 op: op!("!=="),
544 left: Box::new(typeof_expr),
545 right: Box::new(undefined_str_literal),
546 })),
547 op: op!("&&"),
548 right: Box::new(expr.clone()),
549 })
550}
551
552fn rel_filename(base: Option<&Path>, file: &FileName) -> String {
553 let base = match base {
554 Some(v) => v,
555 None => return file.to_string(),
556 };
557
558 let file = match file {
559 FileName::Real(v) => v,
560 _ => {
561 return file.to_string();
562 }
563 };
564
565 let rel_path = diff_paths(file, base);
566
567 let rel_path = match rel_path {
568 Some(v) => v,
569 None => return file.display().to_string(),
570 };
571
572 rel_path.display().to_string()
573}
574
575fn with_transition(transition_name: &str) -> ObjectLit {
576 with_clause(&[("turbopack-transition", transition_name)])
577}
578
579fn with_transition_chunking_type(transition_name: &str, chunking_type: &str) -> Box<ObjectLit> {
580 Box::new(with_clause(&[
581 ("turbopack-transition", transition_name),
582 ("turbopack-chunking-type", chunking_type),
583 ]))
584}
585
586fn with_clause<'a>(entries: impl IntoIterator<Item = &'a (&'a str, &'a str)>) -> ObjectLit {
587 ObjectLit {
588 span: DUMMY_SP,
589 props: entries.into_iter().map(|(k, v)| with_prop(k, v)).collect(),
590 }
591}
592
593fn with_prop(key: &str, value: &str) -> PropOrSpread {
594 PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
595 key: PropName::Str(key.into()),
596 value: Box::new(Expr::Lit(value.into())),
597 })))
598}