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