next_custom_transforms/transforms/
track_dynamic_imports.rs

1use swc_core::{
2    common::{source_map::PURE_SP, util::take::Take, Mark, SyntaxContext},
3    ecma::{
4        ast::*,
5        utils::{prepend_stmt, private_ident, quote_ident, quote_str},
6        visit::{noop_visit_mut_type, visit_mut_pass, VisitMut, VisitMutWith},
7    },
8    quote,
9};
10
11pub fn track_dynamic_imports(unresolved_mark: Mark) -> impl VisitMut + Pass {
12    visit_mut_pass(ImportReplacer::new(unresolved_mark))
13}
14
15struct ImportReplacer {
16    unresolved_ctxt: SyntaxContext,
17    has_dynamic_import: bool,
18    wrapper_function_local_ident: Ident,
19}
20
21impl ImportReplacer {
22    pub fn new(unresolved_mark: Mark) -> Self {
23        ImportReplacer {
24            unresolved_ctxt: SyntaxContext::empty().apply_mark(unresolved_mark),
25            has_dynamic_import: false,
26            wrapper_function_local_ident: private_ident!("$$trackDynamicImport__"),
27        }
28    }
29}
30
31impl VisitMut for ImportReplacer {
32    noop_visit_mut_type!();
33
34    fn visit_mut_program(&mut self, program: &mut Program) {
35        program.visit_mut_children_with(self);
36        // if we wrapped a dynamic import while visiting the children, we need to import the wrapper
37
38        if self.has_dynamic_import {
39            let import_args = MakeNamedImportArgs {
40                original_ident: quote_ident!("trackDynamicImport").into(),
41                local_ident: self.wrapper_function_local_ident.clone(),
42                source: "private-next-rsc-track-dynamic-import",
43                unresolved_ctxt: self.unresolved_ctxt,
44            };
45            match program {
46                Program::Module(module) => {
47                    prepend_stmt(&mut module.body, make_named_import_esm(import_args));
48                }
49                Program::Script(script) => {
50                    // CJS modules can still use `import()`. for CJS, we have to inject the helper
51                    // using `require` instead of `import` to avoid accidentally turning them
52                    // into ESM modules.
53                    prepend_stmt(&mut script.body, make_named_import_cjs(import_args));
54                }
55            }
56        }
57    }
58
59    fn visit_mut_expr(&mut self, expr: &mut Expr) {
60        expr.visit_mut_children_with(self);
61
62        // before: `import(...)`
63        // after:  `$$trackDynamicImport__(import(...))`
64
65        if let Expr::Call(CallExpr {
66            callee: Callee::Import(_),
67            ..
68        }) = expr
69        {
70            self.has_dynamic_import = true;
71            let replacement_expr = quote!(
72                "$wrapper_fn($expr)" as Expr,
73                wrapper_fn = self.wrapper_function_local_ident.clone(),
74                expr: Expr = expr.take()
75            )
76            .with_span(PURE_SP);
77            *expr = replacement_expr
78        }
79    }
80}
81
82struct MakeNamedImportArgs<'a> {
83    original_ident: Ident,
84    local_ident: Ident,
85    source: &'a str,
86    unresolved_ctxt: SyntaxContext,
87}
88
89fn make_named_import_esm(args: MakeNamedImportArgs) -> ModuleItem {
90    let MakeNamedImportArgs {
91        original_ident,
92        local_ident,
93        source,
94        ..
95    } = args;
96    let mut item = quote!(
97        "import { $original_ident as $local_ident } from 'dummy'" as ModuleItem,
98        original_ident = original_ident,
99        local_ident = local_ident,
100    );
101    // the import source cannot be parametrized in `quote!()`, so patch it manually
102    let decl = item.as_mut_module_decl().unwrap().as_mut_import().unwrap();
103    decl.src = Box::new(source.into());
104    item
105}
106
107fn make_named_import_cjs(args: MakeNamedImportArgs) -> Stmt {
108    let MakeNamedImportArgs {
109        original_ident,
110        local_ident,
111        source,
112        unresolved_ctxt,
113    } = args;
114    quote!(
115        "const { [$original_name]: $local_ident } = $require($source)" as Stmt,
116        original_name: Expr = quote_str!(original_ident.sym).into(),
117        local_ident = local_ident,
118        source: Expr = quote_str!(source).into(),
119        // the builtin `require` is considered an unresolved identifier.
120        // we have to match that, or it won't be recognized as
121        // a proper `require()` call.
122        require = quote_ident!(unresolved_ctxt, "require")
123    )
124}