Skip to main content

turbopack_ecmascript/references/
cjs.rs

1use anyhow::Result;
2use bincode::{Decode, Encode};
3use swc_core::{
4    common::{DUMMY_SP, util::take::Take},
5    ecma::{
6        ast::{
7            CallExpr, Expr, ExprOrSpread, Lit, ObjectLit, Prop, PropName, PropOrSpread,
8            SpreadElement,
9        },
10        utils::prop_name_eq,
11    },
12    quote,
13};
14use turbo_rcstr::{RcStr, rcstr};
15use turbo_tasks::{NonLocalValue, ResolvedVc, ValueToString, Vc, debug::ValueDebugFormat};
16use turbopack_core::{
17    chunk::{ChunkingContext, ChunkingType},
18    issue::IssueSource,
19    module::Module,
20    reference::ModuleReference,
21    reference_type::CommonJsReferenceSubType,
22    resolve::{
23        BindingUsage, ExportUsage, ImportUsage, ModuleResolveResult, ResolveErrorMode,
24        origin::ResolveOrigin, parse::Request,
25    },
26};
27use turbopack_resolve::ecmascript::cjs_resolve;
28
29use crate::{
30    ast_path_trie::{AstPathId, AstPathTrie, AstPathTrieBuilder},
31    chunk::{EcmascriptChunkPlaceable, EcmascriptExports},
32    code_gen::{CodeGen, CodeGeneration, IntoCodeGenReference},
33    create_visitor,
34    references::{
35        pattern_mapping::{PatternMapping, ResolveType},
36        util::SpecifiedChunkingType,
37    },
38};
39
40/// Generic CommonJS reference that doesn't perform any codegen. Used for tracing
41#[turbo_tasks::value]
42#[derive(Hash, Debug, ValueToString)]
43#[value_to_string("generic commonjs {request}")]
44pub struct CjsAssetReference {
45    pub origin: ResolvedVc<Box<dyn ResolveOrigin>>,
46    pub request: ResolvedVc<Request>,
47    pub issue_source: IssueSource,
48    pub error_mode: ResolveErrorMode,
49}
50
51#[turbo_tasks::value_impl]
52impl CjsAssetReference {
53    #[turbo_tasks::function]
54    pub fn new(
55        origin: ResolvedVc<Box<dyn ResolveOrigin>>,
56        request: ResolvedVc<Request>,
57        issue_source: IssueSource,
58        error_mode: ResolveErrorMode,
59    ) -> Result<Vc<Self>> {
60        Ok(Self::cell(CjsAssetReference {
61            origin,
62            request,
63            issue_source,
64            error_mode,
65        }))
66    }
67}
68
69#[turbo_tasks::value_impl]
70impl ModuleReference for CjsAssetReference {
71    #[turbo_tasks::function]
72    fn resolve_reference(&self) -> Vc<ModuleResolveResult> {
73        cjs_resolve(
74            *self.origin,
75            *self.request,
76            CommonJsReferenceSubType::Undefined,
77            Some(self.issue_source),
78            self.error_mode,
79        )
80    }
81
82    fn chunking_type(&self) -> Option<ChunkingType> {
83        Some(ChunkingType::Parallel {
84            inherit_async: false,
85            hoisted: false,
86        })
87    }
88
89    fn source(&self) -> Option<IssueSource> {
90        Some(self.issue_source)
91    }
92}
93
94#[turbo_tasks::value(shared)]
95#[derive(Hash, Debug, ValueToString)]
96#[value_to_string("require {request}")]
97pub struct CjsRequireAssetReference {
98    origin: ResolvedVc<Box<dyn ResolveOrigin>>,
99    request: ResolvedVc<Request>,
100    issue_source: IssueSource,
101    error_mode: ResolveErrorMode,
102    chunking_type_attribute: Option<SpecifiedChunkingType>,
103    resolve_override: Option<ResolvedVc<Box<dyn Module>>>,
104    usage: ExportUsage,
105    cjs_tree_shaking: bool,
106}
107
108impl CjsRequireAssetReference {
109    pub fn new(
110        origin: ResolvedVc<Box<dyn ResolveOrigin>>,
111        request: ResolvedVc<Request>,
112        issue_source: IssueSource,
113        error_mode: ResolveErrorMode,
114        chunking_type_attribute: Option<SpecifiedChunkingType>,
115        resolve_override: Option<ResolvedVc<Box<dyn Module>>>,
116        usage: ExportUsage,
117        cjs_tree_shaking: bool,
118    ) -> Self {
119        CjsRequireAssetReference {
120            origin,
121            request,
122            issue_source,
123            error_mode,
124            chunking_type_attribute,
125            resolve_override,
126            usage,
127            cjs_tree_shaking,
128        }
129    }
130}
131
132#[turbo_tasks::value_impl]
133impl ModuleReference for CjsRequireAssetReference {
134    #[turbo_tasks::function]
135    fn resolve_reference(&self) -> Vc<ModuleResolveResult> {
136        if let Some(resolved) = &self.resolve_override {
137            return *ModuleResolveResult::module(*resolved);
138        }
139
140        cjs_resolve(
141            *self.origin,
142            *self.request,
143            CommonJsReferenceSubType::Undefined,
144            Some(self.issue_source),
145            self.error_mode,
146        )
147    }
148
149    fn chunking_type(&self) -> Option<ChunkingType> {
150        self.chunking_type_attribute.map_or_else(
151            || {
152                Some(ChunkingType::Parallel {
153                    inherit_async: false,
154                    hoisted: false,
155                })
156            },
157            |c| c.as_chunking_type(false, false),
158        )
159    }
160
161    fn binding_usage(&self) -> BindingUsage {
162        BindingUsage {
163            import: ImportUsage::TopLevel,
164            export: self.usage.clone(),
165        }
166    }
167
168    fn source(&self) -> Option<IssueSource> {
169        Some(self.issue_source)
170    }
171}
172
173impl IntoCodeGenReference for CjsRequireAssetReference {
174    fn into_reference(self) -> ResolvedVc<Box<dyn ModuleReference>> {
175        ResolvedVc::upcast(self.resolved_cell())
176    }
177
178    fn into_code_gen_reference(
179        self,
180        _trie: &AstPathTrieBuilder,
181        path: AstPathId,
182    ) -> (ResolvedVc<Box<dyn ModuleReference>>, CodeGen) {
183        let reference = self.resolved_cell();
184        (
185            ResolvedVc::upcast(reference),
186            CodeGen::CjsRequireAssetReferenceCodeGen(CjsRequireAssetReferenceCodeGen {
187                reference,
188                path,
189            }),
190        )
191    }
192}
193
194#[derive(PartialEq, Eq, ValueDebugFormat, NonLocalValue, Hash, Debug, Encode, Decode)]
195pub struct CjsRequireAssetReferenceCodeGen {
196    reference: ResolvedVc<CjsRequireAssetReference>,
197    path: AstPathId,
198}
199
200impl CjsRequireAssetReferenceCodeGen {
201    pub async fn code_generation(
202        &self,
203        trie: &AstPathTrie,
204        chunking_context: Vc<Box<dyn ChunkingContext>>,
205    ) -> Result<CodeGeneration> {
206        let reference = self.reference.await?;
207
208        let pm = PatternMapping::resolve_request(
209            *reference.request,
210            *reference.origin,
211            chunking_context,
212            self.reference.resolve_reference(),
213            ResolveType::ChunkItem,
214            Some(Vc::upcast(*self.reference)),
215        )
216        .await?;
217        let mut visitors = Vec::new();
218
219        visitors.push(create_visitor!(
220            trie,
221            self.path,
222            visit_mut_expr,
223            |expr: &mut Expr| {
224                let old_expr = expr.take();
225                let message = if let Expr::Call(CallExpr { args, .. }) = old_expr {
226                    match args.into_iter().next() {
227                        Some(ExprOrSpread {
228                            spread: None,
229                            expr: key_expr,
230                        }) => {
231                            *expr = pm.create_require(*key_expr);
232                            return;
233                        }
234                        Some(ExprOrSpread {
235                            spread: Some(_),
236                            expr: _,
237                        }) => "spread operator is not analyze-able in require() expressions.",
238                        _ => "require() expressions require at least 1 argument",
239                    }
240                } else {
241                    "visitor must be executed on a CallExpr"
242                };
243                *expr = quote!(
244                    "(() => { throw new Error($message); })()" as Expr,
245                    message: Expr = Expr::Lit(Lit::Str(message.into()))
246                );
247            }
248        ));
249
250        Ok(CodeGeneration::visitors(visitors))
251    }
252}
253
254#[turbo_tasks::value]
255#[derive(Hash, Debug, ValueToString)]
256#[value_to_string("require.resolve {request}")]
257pub struct CjsRequireResolveAssetReference {
258    origin: ResolvedVc<Box<dyn ResolveOrigin>>,
259    request: ResolvedVc<Request>,
260    issue_source: IssueSource,
261    error_mode: ResolveErrorMode,
262    chunking_type_attribute: Option<SpecifiedChunkingType>,
263    resolve_override: Option<ResolvedVc<Box<dyn Module>>>,
264}
265
266impl CjsRequireResolveAssetReference {
267    pub fn new(
268        origin: ResolvedVc<Box<dyn ResolveOrigin>>,
269        request: ResolvedVc<Request>,
270        issue_source: IssueSource,
271        error_mode: ResolveErrorMode,
272        chunking_type_attribute: Option<SpecifiedChunkingType>,
273        resolve_override: Option<ResolvedVc<Box<dyn Module>>>,
274    ) -> Self {
275        CjsRequireResolveAssetReference {
276            origin,
277            request,
278            issue_source,
279            error_mode,
280            chunking_type_attribute,
281            resolve_override,
282        }
283    }
284}
285
286#[turbo_tasks::value_impl]
287impl ModuleReference for CjsRequireResolveAssetReference {
288    #[turbo_tasks::function]
289    fn resolve_reference(&self) -> Vc<ModuleResolveResult> {
290        if let Some(resolved) = &self.resolve_override {
291            return *ModuleResolveResult::module(*resolved);
292        }
293
294        cjs_resolve(
295            *self.origin,
296            *self.request,
297            CommonJsReferenceSubType::Undefined,
298            Some(self.issue_source),
299            self.error_mode,
300        )
301    }
302
303    fn chunking_type(&self) -> Option<ChunkingType> {
304        self.chunking_type_attribute.map_or_else(
305            || {
306                Some(ChunkingType::Parallel {
307                    inherit_async: false,
308                    hoisted: false,
309                })
310            },
311            |c| c.as_chunking_type(false, false),
312        )
313    }
314
315    fn source(&self) -> Option<IssueSource> {
316        Some(self.issue_source)
317    }
318}
319
320impl IntoCodeGenReference for CjsRequireResolveAssetReference {
321    fn into_reference(self) -> ResolvedVc<Box<dyn ModuleReference>> {
322        ResolvedVc::upcast(self.resolved_cell())
323    }
324
325    fn into_code_gen_reference(
326        self,
327        _trie: &AstPathTrieBuilder,
328        path: AstPathId,
329    ) -> (ResolvedVc<Box<dyn ModuleReference>>, CodeGen) {
330        let reference = self.resolved_cell();
331        (
332            ResolvedVc::upcast(reference),
333            CodeGen::CjsRequireResolveAssetReferenceCodeGen(
334                CjsRequireResolveAssetReferenceCodeGen { reference, path },
335            ),
336        )
337    }
338}
339
340#[derive(PartialEq, Eq, ValueDebugFormat, NonLocalValue, Hash, Debug, Encode, Decode)]
341pub struct CjsRequireResolveAssetReferenceCodeGen {
342    reference: ResolvedVc<CjsRequireResolveAssetReference>,
343    path: AstPathId,
344}
345
346impl CjsRequireResolveAssetReferenceCodeGen {
347    pub async fn code_generation(
348        &self,
349        trie: &AstPathTrie,
350        chunking_context: Vc<Box<dyn ChunkingContext>>,
351    ) -> Result<CodeGeneration> {
352        let reference = self.reference.await?;
353
354        let pm = PatternMapping::resolve_request(
355            *reference.request,
356            *reference.origin,
357            chunking_context,
358            self.reference.resolve_reference(),
359            ResolveType::ChunkItem,
360            Some(Vc::upcast(*self.reference)),
361        )
362        .await?;
363        let mut visitors = Vec::new();
364
365        // Inline the result of the `require.resolve` call as a literal.
366        visitors.push(create_visitor!(
367            trie,
368            self.path,
369            visit_mut_expr,
370            |expr: &mut Expr| {
371                if let Expr::Call(call_expr) = expr {
372                    let args = std::mem::take(&mut call_expr.args);
373                    *expr = match args.into_iter().next() {
374                        Some(ExprOrSpread { expr, spread: None }) => pm.create_id(*expr),
375                        other => {
376                            let message = match other {
377                                // These are SWC bugs: https://github.com/swc-project/swc/issues/5394
378                                Some(ExprOrSpread {
379                                    spread: Some(_),
380                                    expr: _,
381                                }) => {
382                                    "spread operator is not analyze-able in require() expressions."
383                                }
384                                _ => "require() expressions require at least 1 argument",
385                            };
386                            quote!(
387                                "(() => { throw new Error($message); })()" as Expr,
388                                message: Expr = Expr::Lit(Lit::Str(message.into()))
389                            )
390                        }
391                    };
392                }
393                // CjsRequireResolveAssetReference will only be used for Expr::Call.
394                // Due to eventual consistency the path might match something else,
395                // but we can ignore that as it will be recomputed anyway.
396            }
397        ));
398
399        Ok(CodeGeneration::visitors(visitors))
400    }
401}
402
403/// Removes each named CommonJS export the module graph proved unused. Built by the
404/// analyzer for statically-analyzable CommonJS modules; recognition happens inline
405/// during the walk (see `analyzer::graph::visitor`).
406#[derive(PartialEq, Eq, ValueDebugFormat, NonLocalValue, Hash, Debug, Encode, Decode)]
407pub struct CjsExportsDropCodeGen {
408    drops: Vec<DroppableCjsExportAssignment>,
409    /// Writes to a discarded exports object, dropped whatever the export usage is.
410    dead_writes: Vec<DroppableCjsExportAssignment>,
411    /// Whether the module sets `__esModule`. Without it, a default import binds
412    /// the whole `module.exports`, so nothing may be dropped.
413    has_es_module: bool,
414}
415
416/// A recognized CommonJS export declaration, and thus how it's dropped.
417#[derive(PartialEq, Eq, ValueDebugFormat, NonLocalValue, Hash, Debug, Encode, Decode)]
418pub enum DroppableCjsExportAssignment {
419    /// A standalone `exports.NAME = …` write or `Object.defineProperty(exports, …)`
420    /// call (the assignment is replaced by its value; the define call is removed).
421    Write { name: RcStr, path: AstPathId },
422    /// A `module.exports = { … }` object literal. Every recognized property name
423    /// shares `path` (the assignment), so the literal is rewritten in one pass.
424    ObjectLiteral { names: Vec<RcStr>, path: AstPathId },
425}
426
427impl CjsExportsDropCodeGen {
428    pub fn new(
429        drops: Vec<DroppableCjsExportAssignment>,
430        dead_writes: Vec<DroppableCjsExportAssignment>,
431        has_es_module: bool,
432    ) -> Self {
433        CjsExportsDropCodeGen {
434            drops,
435            dead_writes,
436            has_es_module,
437        }
438    }
439
440    pub async fn code_generation(
441        &self,
442        trie: &AstPathTrie,
443        chunking_context: Vc<Box<dyn ChunkingContext>>,
444        module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
445        _exports: ResolvedVc<EcmascriptExports>,
446    ) -> Result<CodeGeneration> {
447        let export_usage_info = chunking_context
448            .module_export_usage(*ResolvedVc::upcast(module))
449            .await?;
450        let export_usage_info = export_usage_info.export_usage.await?;
451
452        // Without `__esModule`, a default import binds the whole `module.exports`,
453        // so a used `default` makes every named export reachable — drop none of these.
454        let drops = if !self.has_es_module && export_usage_info.is_export_used(&rcstr!("default")) {
455            &[]
456        } else {
457            &self.drops[..]
458        };
459
460        // Replace each unused `exports.NAME = <value>` with `<value>`, preserving
461        // side effects (the minifier drops a pure value). Rewriting the assignment
462        // rather than its statement keeps chained writes like
463        // `exports.a = exports.b = 1` sound.
464        let mut visitors = Vec::new();
465        for (drop, dead) in (self.dead_writes.iter().map(|drop| (drop, true)))
466            .chain(drops.iter().map(|drop| (drop, false)))
467        {
468            match drop {
469                DroppableCjsExportAssignment::Write { name, path } => {
470                    if !dead && export_usage_info.is_export_used(name) {
471                        continue;
472                    }
473                    visitors.push(create_visitor!(
474                        trie,
475                        *path,
476                        visit_mut_expr,
477                        |expr: &mut Expr| {
478                            match expr {
479                                // `exports.NAME = <value>` → `<value>` (keep side effects).
480                                Expr::Assign(assign) => {
481                                    let value = assign.right.take();
482                                    *expr = *value;
483                                }
484                                // `Object.defineProperty(exports, …)`: keep an eager
485                                // `value`'s side effects; a getter is lazy, drop the call.
486                                Expr::Call(call) => {
487                                    *expr = match take_define_property_value(call) {
488                                        Some(value) => *value,
489                                        None => quote!("0" as Expr),
490                                    };
491                                }
492                                _ => {}
493                            }
494                        }
495                    ));
496                }
497                DroppableCjsExportAssignment::ObjectLiteral { names, path } => {
498                    let unused = names
499                        .iter()
500                        .filter(|name| dead || !export_usage_info.is_export_used(name))
501                        .cloned()
502                        .collect::<Vec<_>>();
503                    if unused.is_empty() {
504                        continue;
505                    }
506                    // `module.exports = { …, NAME: v, … }` → drop each unused `NAME`,
507                    // keeping a data value's side effects in place via `...(void v)`.
508                    visitors.push(create_visitor!(
509                        trie,
510                        *path,
511                        visit_mut_expr,
512                        |expr: &mut Expr| {
513                            if let Expr::Assign(assign) = expr
514                                && let Expr::Object(obj) = &mut *assign.right
515                            {
516                                drop_object_literal_exports(obj, &unused);
517                            }
518                        }
519                    ));
520                }
521            }
522        }
523
524        Ok(CodeGeneration::visitors(visitors))
525    }
526}
527
528/// Takes the `value: <expr>` out of an `Object.defineProperty` descriptor, if it
529/// has one. A descriptor without `value` is a getter, so there's nothing to keep.
530fn take_define_property_value(call: &mut CallExpr) -> Option<Box<Expr>> {
531    let descriptor = call.args.get_mut(2)?;
532    let Expr::Object(descriptor) = &mut *descriptor.expr else {
533        return None;
534    };
535    descriptor.props.iter_mut().find_map(|prop| {
536        let PropOrSpread::Prop(prop) = prop else {
537            return None;
538        };
539        let Prop::KeyValue(kv) = &mut **prop else {
540            return None;
541        };
542        prop_name_eq(&kv.key, "value").then(|| kv.value.take())
543    })
544}
545
546/// Drops each of `names` from a `module.exports = { … }` literal.
547fn drop_object_literal_exports(obj: &mut ObjectLit, names: &[RcStr]) {
548    let is_dropped = |key: &PropName| names.iter().any(|n| prop_name_eq(key, n));
549    obj.props = obj
550        .props
551        .take()
552        .into_iter()
553        .filter_map(|prop| {
554            let PropOrSpread::Prop(p) = &prop else {
555                return Some(prop);
556            };
557            match &**p {
558                // The value might have a side effect, preserve it by generating `...void (expr)`
559                Prop::KeyValue(kv) if is_dropped(&kv.key) => {
560                    Some(PropOrSpread::Spread(SpreadElement {
561                        dot3_token: DUMMY_SP,
562                        expr: Box::new(quote!("void ($e)" as Expr, e: Expr = *kv.value.clone())),
563                    }))
564                }
565                Prop::Shorthand(id) if names.iter().any(|n| id.sym.as_str() == &**n) => None,
566                Prop::Getter(g) if is_dropped(&g.key) => None,
567                Prop::Setter(s) if is_dropped(&s.key) => None,
568                Prop::Method(m) if is_dropped(&m.key) => None,
569                // A used export (or an already-rewritten spread) — keep as-is.
570                _ => Some(prop),
571            }
572        })
573        .collect();
574}
575
576impl From<CjsExportsDropCodeGen> for CodeGen {
577    fn from(val: CjsExportsDropCodeGen) -> Self {
578        CodeGen::CjsExportsDropCodeGen(val)
579    }
580}