Skip to main content

turbopack_ecmascript/references/
require_context.rs

1use std::{borrow::Cow, collections::VecDeque, sync::Arc};
2
3use anyhow::{Result, bail};
4use bincode::{Decode, Encode};
5use swc_core::{
6    common::DUMMY_SP,
7    ecma::{
8        ast::{
9            Expr, ExprStmt, KeyValueProp, Lit, ModuleItem, ObjectLit, Prop, PropName, PropOrSpread,
10            Stmt, {self},
11        },
12        codegen::{Emitter, text_writer::JsWriter},
13    },
14    quote, quote_expr,
15};
16use turbo_esregex::EsRegex;
17use turbo_rcstr::RcStr;
18use turbo_tasks::{
19    FxIndexMap, NonLocalValue, ResolvedVc, ValueToString, Vc, debug::ValueDebugFormat,
20    trace::TraceRawVcs,
21};
22use turbo_tasks_fs::{DirectoryContent, DirectoryEntry, FileSystemPath};
23use turbopack_core::{
24    chunk::{
25        AsyncModuleInfo, ChunkableModule, ChunkingContext, ChunkingType, MinifyType,
26        ModuleChunkItemIdExt,
27    },
28    ident::AssetIdent,
29    issue::IssueSource,
30    module::{Module, ModuleSideEffects},
31    module_graph::ModuleGraph,
32    reference::{ModuleReference, ModuleReferences},
33    reference_type::CommonJsReferenceSubType,
34    resolve::{ModuleResolveResult, ResolveErrorMode, origin::ResolveOrigin, parse::Request},
35    source::Source,
36};
37use turbopack_resolve::ecmascript::cjs_resolve;
38
39use crate::{
40    EcmascriptChunkPlaceable,
41    chunk::{EcmascriptChunkItemContent, EcmascriptExports, ecmascript_chunk_item},
42    code_gen::{CodeGen, CodeGeneration, IntoCodeGenReference},
43    create_visitor,
44    references::{
45        AstPath,
46        pattern_mapping::{PatternMapping, ResolveType},
47    },
48    runtime_functions::{TURBOPACK_EXPORT_VALUE, TURBOPACK_MODULE_CONTEXT, TURBOPACK_REQUIRE},
49    utils::module_id_to_lit,
50};
51
52#[turbo_tasks::value]
53#[derive(Debug)]
54pub(crate) enum DirListEntry {
55    File(FileSystemPath),
56    Dir(ResolvedVc<DirList>),
57}
58
59#[turbo_tasks::value(transparent)]
60pub(crate) struct DirList(
61    #[bincode(with = "turbo_bincode::indexmap")] FxIndexMap<RcStr, DirListEntry>,
62);
63
64#[turbo_tasks::value_impl]
65impl DirList {
66    #[turbo_tasks::function]
67    pub(crate) fn read(dir: FileSystemPath, recursive: bool, filter: Vc<EsRegex>) -> Vc<Self> {
68        Self::read_internal(dir.clone(), dir, recursive, filter)
69    }
70
71    #[turbo_tasks::function]
72    async fn read_internal(
73        root: FileSystemPath,
74        dir: FileSystemPath,
75        recursive: bool,
76        filter: Vc<EsRegex>,
77    ) -> Result<Vc<Self>> {
78        let root_val = root.clone();
79        let dir_val = dir.clone();
80        let regex = &filter.await?;
81
82        let mut list = FxIndexMap::default();
83
84        let dir_content = dir.read_dir().await?;
85        let entries = match &*dir_content {
86            DirectoryContent::Entries(entries) => Some(entries),
87            DirectoryContent::NotFound => None,
88        };
89
90        for (_, entry) in entries.iter().flat_map(|m| m.iter()) {
91            match entry {
92                DirectoryEntry::File(path) => {
93                    if let Some(relative_path) = root_val.get_relative_path_to(path) {
94                        // Webpack always chacks the RegExp against a path prefixed with `./`
95                        let relative_path = RcStr::from(format!("./{relative_path}"));
96                        if regex.is_match(&relative_path) {
97                            list.insert(relative_path, DirListEntry::File(path.clone()));
98                        }
99                    }
100                }
101                DirectoryEntry::Directory(path) if recursive => {
102                    if let Some(relative_path) = dir_val.get_relative_path_to(path) {
103                        list.insert(
104                            relative_path,
105                            DirListEntry::Dir(
106                                DirList::read_internal(
107                                    root.clone(),
108                                    path.clone(),
109                                    recursive,
110                                    filter,
111                                )
112                                .to_resolved()
113                                .await?,
114                            ),
115                        );
116                    }
117                }
118                // ignore everything else
119                _ => {}
120            }
121        }
122
123        list.sort_keys();
124
125        Ok(Vc::cell(list))
126    }
127
128    #[turbo_tasks::function]
129    async fn flatten(self: Vc<Self>) -> Result<Vc<FlatDirList>> {
130        let this = self.await?;
131
132        let mut queue = VecDeque::from([this]);
133
134        let mut list = FxIndexMap::default();
135
136        while let Some(dir) = queue.pop_front() {
137            for (k, entry) in &*dir {
138                match entry {
139                    DirListEntry::File(path) => {
140                        list.insert(k.clone(), path.clone());
141                    }
142                    DirListEntry::Dir(d) => {
143                        queue.push_back(d.await?);
144                    }
145                }
146            }
147        }
148
149        Ok(Vc::cell(list))
150    }
151}
152
153#[turbo_tasks::value(transparent)]
154pub(crate) struct FlatDirList(
155    #[bincode(with = "turbo_bincode::indexmap")] FxIndexMap<RcStr, FileSystemPath>,
156);
157
158#[turbo_tasks::value_impl]
159impl FlatDirList {
160    #[turbo_tasks::function]
161    pub(crate) fn read(dir: FileSystemPath, recursive: bool, filter: Vc<EsRegex>) -> Vc<Self> {
162        DirList::read(dir, recursive, filter).flatten()
163    }
164}
165
166#[turbo_tasks::value]
167#[derive(Debug)]
168pub struct RequireContextMapEntry {
169    pub origin_relative: RcStr,
170    pub request: ResolvedVc<Request>,
171    pub result: ResolvedVc<ModuleResolveResult>,
172}
173
174/// The resolved context map for a `require.context(..)` call.
175#[turbo_tasks::value(transparent)]
176pub struct RequireContextMap(
177    #[bincode(with = "turbo_bincode::indexmap")] FxIndexMap<RcStr, RequireContextMapEntry>,
178);
179
180#[turbo_tasks::value_impl]
181impl RequireContextMap {
182    #[turbo_tasks::function]
183    pub(crate) async fn generate(
184        origin: Vc<Box<dyn ResolveOrigin>>,
185        dir: FileSystemPath,
186        recursive: bool,
187        filter: Vc<EsRegex>,
188        issue_source: Option<IssueSource>,
189        error_mode: ResolveErrorMode,
190    ) -> Result<Vc<Self>> {
191        let origin_path = origin.into_trait_ref().await?.origin_path().parent();
192
193        let list = &*FlatDirList::read(dir, recursive, filter).await?;
194
195        let mut map = FxIndexMap::default();
196
197        for (context_relative, path) in list {
198            let Some(origin_relative) = origin_path.get_relative_path_to(path) else {
199                bail!("invariant error: this was already checked in `list_dir`");
200            };
201            let origin_relative = if origin_relative.starts_with("../") {
202                origin_relative
203            } else {
204                RcStr::from(format!("./{origin_relative}"))
205            };
206
207            let request = Request::parse(origin_relative.clone().into())
208                .to_resolved()
209                .await?;
210            let result = cjs_resolve(
211                origin,
212                *request,
213                CommonJsReferenceSubType::Undefined,
214                issue_source,
215                error_mode,
216            )
217            .to_resolved()
218            .await?;
219
220            map.insert(
221                context_relative.clone(),
222                RequireContextMapEntry {
223                    origin_relative,
224                    request,
225                    result,
226                },
227            );
228        }
229
230        Ok(Vc::cell(map))
231    }
232}
233
234/// A reference for `require.context()`, will replace it with an inlined map
235/// wrapped in `__turbopack_module_context__`;
236#[turbo_tasks::value]
237#[derive(Hash, Debug, ValueToString)]
238pub struct RequireContextAssetReference {
239    pub inner: ResolvedVc<RequireContextAsset>,
240    pub dir: RcStr,
241    pub include_subdirs: bool,
242
243    pub issue_source: Option<IssueSource>,
244    pub error_mode: ResolveErrorMode,
245}
246
247impl std::fmt::Display for RequireContextAssetReference {
248    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249        write!(
250            f,
251            "require.context {}/{}",
252            self.dir,
253            if self.include_subdirs { "**" } else { "*" },
254        )
255    }
256}
257
258impl RequireContextAssetReference {
259    pub async fn new(
260        source: ResolvedVc<Box<dyn Source>>,
261        origin: ResolvedVc<Box<dyn ResolveOrigin>>,
262        dir: RcStr,
263        include_subdirs: bool,
264        filter: Vc<EsRegex>,
265        issue_source: Option<IssueSource>,
266        error_mode: ResolveErrorMode,
267    ) -> Result<Self> {
268        let map = RequireContextMap::generate(
269            *origin,
270            origin
271                .into_trait_ref()
272                .await?
273                .origin_path()
274                .parent()
275                .join(&dir)?,
276            include_subdirs,
277            filter,
278            issue_source,
279            error_mode,
280        )
281        .to_resolved()
282        .await?;
283        let inner = RequireContextAsset {
284            source,
285            origin,
286            map,
287
288            dir: dir.clone(),
289            include_subdirs,
290        }
291        .resolved_cell();
292
293        Ok(RequireContextAssetReference {
294            inner,
295            dir,
296            include_subdirs,
297            issue_source,
298            error_mode,
299        })
300    }
301}
302
303#[turbo_tasks::value_impl]
304impl ModuleReference for RequireContextAssetReference {
305    #[turbo_tasks::function]
306    fn resolve_reference(&self) -> Vc<ModuleResolveResult> {
307        *ModuleResolveResult::module(ResolvedVc::upcast(self.inner))
308    }
309
310    fn chunking_type(&self) -> Option<ChunkingType> {
311        Some(ChunkingType::Parallel {
312            inherit_async: false,
313            hoisted: false,
314        })
315    }
316
317    fn source(&self) -> Option<IssueSource> {
318        self.issue_source
319    }
320}
321
322impl IntoCodeGenReference for RequireContextAssetReference {
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::RequireContextAssetReferenceCodeGen(RequireContextAssetReferenceCodeGen {
335                reference,
336                path,
337            }),
338        )
339    }
340}
341
342#[derive(
343    PartialEq, Eq, TraceRawVcs, ValueDebugFormat, NonLocalValue, Hash, Debug, Encode, Decode,
344)]
345pub struct RequireContextAssetReferenceCodeGen {
346    path: AstPath,
347    reference: ResolvedVc<RequireContextAssetReference>,
348}
349
350impl RequireContextAssetReferenceCodeGen {
351    pub async fn code_generation(
352        &self,
353        chunking_context: Vc<Box<dyn ChunkingContext>>,
354    ) -> Result<CodeGeneration> {
355        let module_id = self
356            .reference
357            .await?
358            .inner
359            .chunk_item_id(chunking_context)
360            .await?;
361
362        let mut visitors = Vec::new();
363
364        visitors.push(create_visitor!(
365            self.path,
366            visit_mut_expr,
367            |expr: &mut Expr| {
368                if let Expr::Call(_) = expr {
369                    *expr = quote!(
370                        "$turbopack_module_context($turbopack_require($id))" as Expr,
371                        turbopack_module_context: Expr = TURBOPACK_MODULE_CONTEXT.into(),
372                        turbopack_require: Expr = TURBOPACK_REQUIRE.into(),
373                        id: Expr = module_id_to_lit(&module_id)
374                    );
375                }
376            }
377        ));
378
379        Ok(CodeGeneration::visitors(visitors))
380    }
381}
382
383#[turbo_tasks::value(transparent)]
384#[derive(ValueToString)]
385#[value_to_string("resolved reference")]
386pub struct ResolvedModuleReference(ResolvedVc<ModuleResolveResult>);
387
388#[turbo_tasks::value_impl]
389impl ModuleReference for ResolvedModuleReference {
390    #[turbo_tasks::function]
391    fn resolve_reference(&self) -> Vc<ModuleResolveResult> {
392        *self.0
393    }
394
395    fn chunking_type(&self) -> Option<ChunkingType> {
396        Some(ChunkingType::Parallel {
397            inherit_async: false,
398            hoisted: false,
399        })
400    }
401}
402
403#[turbo_tasks::value]
404pub struct RequireContextAsset {
405    source: ResolvedVc<Box<dyn Source>>,
406
407    origin: ResolvedVc<Box<dyn ResolveOrigin>>,
408    map: ResolvedVc<RequireContextMap>,
409
410    dir: RcStr,
411    include_subdirs: bool,
412}
413
414fn modifier(dir: &RcStr, include_subdirs: bool) -> RcStr {
415    format!(
416        "require.context {}/{}",
417        dir,
418        if include_subdirs { "**" } else { "*" },
419    )
420    .into()
421}
422
423#[turbo_tasks::value_impl]
424impl Module for RequireContextAsset {
425    #[turbo_tasks::function]
426    async fn ident(&self) -> Result<Vc<AssetIdent>> {
427        Ok(self
428            .source
429            .ident()
430            .owned()
431            .await?
432            .with_modifier(modifier(&self.dir, self.include_subdirs))
433            .into_vc())
434    }
435
436    #[turbo_tasks::function]
437    fn source(&self) -> Vc<turbopack_core::source::OptionSource> {
438        Vc::cell(Some(self.source))
439    }
440
441    #[turbo_tasks::function]
442    async fn references(&self) -> Result<Vc<ModuleReferences>> {
443        let map = &*self.map.await?;
444
445        Ok(Vc::cell(
446            map.iter()
447                .map(|(_, entry)| {
448                    ResolvedVc::upcast(ResolvedVc::<ResolvedModuleReference>::cell(entry.result))
449                })
450                .collect(),
451        ))
452    }
453
454    #[turbo_tasks::function]
455    fn side_effects(self: Vc<Self>) -> Vc<ModuleSideEffects> {
456        ModuleSideEffects::SideEffectFree.cell()
457    }
458}
459
460#[turbo_tasks::value_impl]
461impl ChunkableModule for RequireContextAsset {
462    #[turbo_tasks::function]
463    fn as_chunk_item(
464        self: ResolvedVc<Self>,
465        module_graph: ResolvedVc<ModuleGraph>,
466        chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
467    ) -> Vc<Box<dyn turbopack_core::chunk::ChunkItem>> {
468        ecmascript_chunk_item(ResolvedVc::upcast(self), module_graph, chunking_context)
469    }
470}
471
472#[turbo_tasks::value_impl]
473impl EcmascriptChunkPlaceable for RequireContextAsset {
474    #[turbo_tasks::function]
475    fn get_exports(&self) -> Vc<EcmascriptExports> {
476        EcmascriptExports::Value.cell()
477    }
478
479    #[turbo_tasks::function]
480    async fn chunk_item_content(
481        &self,
482        chunking_context: Vc<Box<dyn ChunkingContext>>,
483        _module_graph: Vc<ModuleGraph>,
484        _async_module_info: Option<Vc<AsyncModuleInfo>>,
485        _estimated: bool,
486    ) -> Result<Vc<EcmascriptChunkItemContent>> {
487        let map = &*self.map.await?;
488        let minify = chunking_context.minify_type().await?;
489
490        let mut context_map = ObjectLit {
491            span: DUMMY_SP,
492            props: vec![],
493        };
494
495        for (key, entry) in map {
496            let pm = PatternMapping::resolve_request(
497                *entry.request,
498                *self.origin,
499                chunking_context,
500                *entry.result,
501                ResolveType::ChunkItem,
502                None,
503            )
504            .await?;
505
506            let PatternMapping::Single(pm) = &*pm else {
507                continue;
508            };
509
510            let key_expr = Expr::Lit(Lit::Str(entry.origin_relative.as_str().into()));
511
512            let prop = KeyValueProp {
513                key: PropName::Str(key.as_str().into()),
514                value: quote_expr!(
515                    "{ id: () => $id, module: () => $module }",
516                    id: Expr =
517                        pm.create_id(Cow::Borrowed(&key_expr)),
518                    module: Expr =
519                        pm.create_require(Cow::Borrowed(&key_expr)),
520                ),
521            };
522
523            context_map
524                .props
525                .push(PropOrSpread::Prop(Box::new(Prop::KeyValue(prop))));
526        }
527
528        let expr = quote_expr!(
529            "$turbopack_export_value($obj);",
530            turbopack_export_value: Expr = TURBOPACK_EXPORT_VALUE.into(),
531            obj: Expr = Expr::Object(context_map),
532        );
533
534        let module = ast::Module {
535            span: DUMMY_SP,
536            body: vec![ModuleItem::Stmt(Stmt::Expr(ExprStmt {
537                span: DUMMY_SP,
538                expr,
539            }))],
540            shebang: None,
541        };
542
543        let source_map: Arc<swc_core::common::SourceMap> = Default::default();
544
545        let mut bytes: Vec<u8> = vec![];
546        let mut wr: JsWriter<'_, &mut Vec<u8>> =
547            JsWriter::new(source_map.clone(), "\n", &mut bytes, None);
548        if matches!(*minify, MinifyType::Minify { .. }) {
549            wr.set_indent_str("");
550        }
551
552        let mut emitter = Emitter {
553            cfg: swc_core::ecma::codegen::Config::default(),
554            cm: source_map.clone(),
555            comments: None,
556            wr,
557        };
558
559        emitter.emit_module(&module)?;
560
561        Ok(EcmascriptChunkItemContent {
562            inner_code: bytes.into(),
563            ..Default::default()
564        }
565        .cell())
566    }
567}