next_custom_transforms/transforms/
dynamic.rs

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
23/// Creates a SWC visitor to transform `next/dynamic` calls to have the
24/// corresponding `loadableGenerated` property.
25///
26/// **NOTE** We do not use `NextDynamicMode::Turbopack` yet. It isn't compatible
27/// with current loadable manifest, which causes hydration errors.
28pub 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    /// In Webpack mode, each `dynamic()` call will generate a key composed
64    /// from:
65    /// 1. The current module's path relative to the pages directory;
66    /// 2. The relative imported module id.
67    ///
68    /// This key is of the form:
69    /// {currentModulePath} -> {relativeImportedModulePath}
70    ///
71    /// It corresponds to an entry in the React Loadable Manifest generated by
72    /// the React Loadable Webpack plugin.
73    Webpack,
74    /// In Turbopack mode:
75    /// * each dynamic import is amended with a transition to `dynamic_transition_name`
76    /// * the ident of the client module (via `dynamic_client_transition_name`) is added to the
77    ///   metadata
78    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    /// In Turbo mode, contains a list of modules that need to be imported with
102    /// the given transition under a particular ident.
103    #[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    // TODO do we need more variants? server vs client vs dev vs prod?
114    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                                // dev client or server:
221                                // loadableGenerated: {
222                                //   modules:
223                                // ["/project/src/file-being-transformed.js -> " +
224                                // '../components/hello'] }
225                                //
226                                // prod client
227                                // loadableGenerated: {
228                                //   webpack: () => [require.resolveWeak('../components/hello')],
229                                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                                // loadableGenerated: { modules: [
249                                // ".../client.js [app-client] (ecmascript, next/dynamic entry)"
250                                // ]}
251                                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                            // Only use `require.resolveWebpack` to decouple modules for webpack,
317                            // turbopack doesn't need this
318
319                            // When it's not preferring to picking up ESM (in the pages router), we
320                            // don't need to do it as it doesn't need to enter the non-ssr module.
321                            //
322                            // Also transforming it to `require.resolveWeak` doesn't work with ESM
323                            // imports ( i.e. require.resolveWeak(esm asset)).
324                            if should_skip_ssr_compile {
325                                // if it's server components SSR layer
326                                // Transform 1st argument `expr.args[0]` aka the module loader from:
327                                // dynamic(() => import('./client-mod'), { ssr: false }))`
328                                // into:
329                                // dynamic(async () => {
330                                //   require.resolveWeak('./client-mod')
331                                // }, { ssr: false }))`
332
333                                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                            // When `ssr: false`
373                            // if it's server components SSR layer
374                            // Transform 1st argument `expr.args[0]` aka the module loader from:
375                            // dynamic(() => import('./client-mod'), { ssr: false }))`
376                            // into:
377                            // dynamic(async () => {}, { ssr: false }))`
378                            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                                // Add `{with:{turbopack-transition: ...}}` to the dynamic import
395                                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}
426// Add `{with:{turbopack-transition: <self.transition_name>}}` to any dynamic imports
427impl 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                    // Turbopack will automatically transform the imported `__turbopack_module_id__`
507                    // identifier into the imported module's id.
508                    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, // 'typeof' operator
551        arg: Box::new(Expr::Ident(Ident {
552            sym: quote_ident!("require.resolveWeak").sym,
553            ..Default::default()
554        })),
555    });
556
557    // typeof require.resolveWeak !== 'undefined' && <expression>
558    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}