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, ArrowFunctionBody, BinExpr, Bool, CallExpr, Callee, Expr,
13 ExprOrSpread, ExprStmt, FunctionBody, Id, Ident, IdentName, ImportDecl,
14 ImportNamedSpecifier, ImportSpecifier, KeyValueProp, Lit, ModuleDecl, ModuleItem,
15 ObjectLit, Pass, Prop, PropName, PropOrSpread, Stmt, Str, Tpl, UnaryExpr, UnaryOp, op,
16 },
17 utils::{ExprFactory, private_ident, prop_name_eq, 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 } && prop_name_eq(key, "ssr")
279 && let Some(Lit::Bool(Bool {
280 value: false,
281 span: _,
282 })) = value.as_lit()
283 {
284 has_ssr_false = true
285 }
286 }
287 props.extend(options_props.iter().cloned());
288 }
289
290 let should_skip_ssr_compile = has_ssr_false
291 && self.is_server_compiler
292 && !self.is_react_server_layer
293 && self.prefer_esm;
294
295 match &self.state {
296 NextDynamicPatcherState::Webpack => {
297 if should_skip_ssr_compile {
306 let require_resolve_weak_expr = Expr::Call(CallExpr {
315 span: DUMMY_SP,
316 callee: quote_ident!("require.resolveWeak").as_callee(),
317 args: vec![ExprOrSpread {
318 spread: None,
319 expr: Box::new(Expr::Lit(Lit::Str(Str {
320 span: DUMMY_SP,
321 value: dynamically_imported_specifier.clone(),
322 raw: None,
323 }))),
324 }],
325 ..Default::default()
326 });
327
328 let side_effect_free_loader_arg = Expr::Arrow(ArrowExpr {
329 span: DUMMY_SP,
330 params: vec![],
331 body: Box::new(ArrowFunctionBody::FunctionBody(FunctionBody {
332 span: DUMMY_SP,
333 stmts: vec![Stmt::Expr(ExprStmt {
334 span: DUMMY_SP,
335 expr: Box::new(exec_expr_when_resolve_weak_available(
336 &require_resolve_weak_expr,
337 )),
338 })],
339 })),
340 is_async: true,
341 is_generator: false,
342 ..Default::default()
343 });
344
345 expr.args[0] = side_effect_free_loader_arg.as_arg();
346 }
347 }
348 NextDynamicPatcherState::Turbopack {
349 dynamic_transition_name,
350 ..
351 } => {
352 if should_skip_ssr_compile {
359 let side_effect_free_loader_arg = Expr::Arrow(ArrowExpr {
360 span: DUMMY_SP,
361 params: vec![],
362 body: Box::new(ArrowFunctionBody::FunctionBody(FunctionBody {
363 span: DUMMY_SP,
364 stmts: vec![],
365 })),
366 is_async: true,
367 is_generator: false,
368 ..Default::default()
369 });
370
371 expr.args[0] = side_effect_free_loader_arg.as_arg();
372 } else {
373 let mut visitor = DynamicImportTransitionAdder {
375 transition_name: dynamic_transition_name,
376 };
377 expr.args[0].visit_mut_with(&mut visitor);
378 }
379 }
380 }
381
382 let second_arg = ExprOrSpread {
383 spread: None,
384 expr: Box::new(Expr::Object(ObjectLit {
385 span: DUMMY_SP,
386 props,
387 })),
388 };
389
390 if expr.args.len() == 2 {
391 expr.args[1] = second_arg;
392 } else {
393 expr.args.push(second_arg)
394 }
395 }
396 }
397}
398
399struct DynamicImportTransitionAdder<'a> {
400 transition_name: &'a str,
401}
402impl VisitMut for DynamicImportTransitionAdder<'_> {
404 fn visit_mut_call_expr(&mut self, expr: &mut CallExpr) {
405 if let Callee::Import(..) = &expr.callee {
406 let options = ExprOrSpread {
407 expr: Box::new(
408 ObjectLit {
409 span: DUMMY_SP,
410 props: vec![PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
411 key: PropName::Ident(IdentName::new(atom!("with"), DUMMY_SP)),
412 value: with_transition(self.transition_name).into(),
413 })))],
414 }
415 .into(),
416 ),
417 spread: None,
418 };
419
420 match expr.args.get_mut(1) {
421 Some(arg) => *arg = options,
422 None => expr.args.push(options),
423 }
424 } else {
425 expr.visit_mut_children_with(self);
426 }
427 }
428}
429
430fn module_id_options(module_id: Expr) -> Vec<PropOrSpread> {
431 vec![PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
432 key: PropName::Ident(IdentName::new(atom!("modules"), DUMMY_SP)),
433 value: Box::new(Expr::Array(ArrayLit {
434 elems: vec![Some(ExprOrSpread {
435 expr: Box::new(module_id),
436 spread: None,
437 })],
438 span: DUMMY_SP,
439 })),
440 })))]
441}
442
443fn webpack_options(module_id: Expr) -> Vec<PropOrSpread> {
444 vec![PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
445 key: PropName::Ident(IdentName::new(atom!("webpack"), DUMMY_SP)),
446 value: Box::new(Expr::Arrow(ArrowExpr {
447 params: vec![],
448 body: Box::new(ArrowFunctionBody::Expr(Box::new(Expr::Array(ArrayLit {
449 elems: vec![Some(ExprOrSpread {
450 expr: Box::new(module_id),
451 spread: None,
452 })],
453 span: DUMMY_SP,
454 })))),
455 is_async: false,
456 is_generator: false,
457 span: DUMMY_SP,
458 ..Default::default()
459 })),
460 })))]
461}
462
463impl NextDynamicPatcher {
464 fn maybe_add_dynamically_imported_specifier(&mut self, items: &mut Vec<ModuleItem>) {
465 let NextDynamicPatcherState::Turbopack {
466 dynamic_client_transition_name,
467 imports,
468 ..
469 } = &mut self.state
470 else {
471 return;
472 };
473
474 let mut new_items = Vec::with_capacity(imports.len());
475
476 for import in std::mem::take(imports) {
477 match import {
478 TurbopackImport::Import {
479 id_ident,
480 specifier,
481 } => {
482 new_items.push(ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
485 span: DUMMY_SP,
486 specifiers: vec![ImportSpecifier::Named(ImportNamedSpecifier {
487 span: DUMMY_SP,
488 local: id_ident,
489 imported: Some(
490 Ident::new(
491 atom!("__turbopack_module_id__"),
492 DUMMY_SP,
493 Default::default(),
494 )
495 .into(),
496 ),
497 is_type_only: false,
498 })],
499 src: Box::new(specifier.into()),
500 type_only: false,
501 with: Some(with_transition_chunking_type(
502 dynamic_client_transition_name,
503 "none",
504 )),
505 phase: Default::default(),
506 })));
507 }
508 }
509 }
510
511 new_items.append(items);
512
513 std::mem::swap(&mut new_items, items)
514 }
515}
516
517fn exec_expr_when_resolve_weak_available(expr: &Expr) -> Expr {
518 let undefined_str_literal = Expr::Lit(Lit::Str(Str {
519 span: DUMMY_SP,
520 value: atom!("undefined").into(),
521 raw: None,
522 }));
523
524 let typeof_expr = Expr::Unary(UnaryExpr {
525 span: DUMMY_SP,
526 op: UnaryOp::TypeOf, arg: Box::new(Expr::Ident(Ident {
528 sym: quote_ident!("require.resolveWeak").sym,
529 ..Default::default()
530 })),
531 });
532
533 Expr::Bin(BinExpr {
535 span: DUMMY_SP,
536 left: Box::new(Expr::Bin(BinExpr {
537 span: DUMMY_SP,
538 op: op!("!=="),
539 left: Box::new(typeof_expr),
540 right: Box::new(undefined_str_literal),
541 })),
542 op: op!("&&"),
543 right: Box::new(expr.clone()),
544 })
545}
546
547fn rel_filename(base: Option<&Path>, file: &FileName) -> String {
548 let base = match base {
549 Some(v) => v,
550 None => return file.to_string(),
551 };
552
553 let file = match file {
554 FileName::Real(v) => v,
555 _ => {
556 return file.to_string();
557 }
558 };
559
560 let rel_path = diff_paths(file, base);
561
562 let rel_path = match rel_path {
563 Some(v) => v,
564 None => return file.display().to_string(),
565 };
566
567 rel_path.display().to_string()
568}
569
570fn with_transition(transition_name: &str) -> ObjectLit {
571 with_clause(&[("turbopack-transition", transition_name)])
572}
573
574fn with_transition_chunking_type(transition_name: &str, chunking_type: &str) -> Box<ObjectLit> {
575 Box::new(with_clause(&[
576 ("turbopack-transition", transition_name),
577 ("turbopack-chunking-type", chunking_type),
578 ]))
579}
580
581fn with_clause<'a>(entries: impl IntoIterator<Item = &'a (&'a str, &'a str)>) -> ObjectLit {
582 ObjectLit {
583 span: DUMMY_SP,
584 props: entries.into_iter().map(|(k, v)| with_prop(k, v)).collect(),
585 }
586}
587
588fn with_prop(key: &str, value: &str) -> PropOrSpread {
589 PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
590 key: PropName::Str(key.into()),
591 value: Box::new(Expr::Lit(value.into())),
592 })))
593}