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