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