Skip to main content

turbopack_ecmascript/references/
import_meta_glob.rs

1use std::{borrow::Cow, sync::Arc};
2
3use anyhow::{Result, bail};
4use bincode::{Decode, Encode};
5use swc_core::{
6    common::{
7        DUMMY_SP, Span,
8        errors::{DiagnosticId, Handler},
9    },
10    ecma::{
11        ast::{
12            Expr, ExprStmt, KeyValueProp, Lit, ModuleItem, ObjectLit, Prop, PropName, PropOrSpread,
13            Stmt, {self},
14        },
15        codegen::{Emitter, text_writer::JsWriter},
16    },
17    quote, quote_expr,
18};
19use turbo_rcstr::RcStr;
20use turbo_tasks::{
21    FxIndexMap, NonLocalValue, ResolvedVc, TryJoinIterExt, ValueToString, Vc,
22    debug::ValueDebugFormat, trace::TraceRawVcs,
23};
24use turbo_tasks_fs::{
25    DirectoryEntry, FileSystemPath, ReadGlobResult,
26    glob::{Glob, GlobOptions},
27};
28use turbopack_core::{
29    chunk::{
30        AsyncModuleInfo, ChunkableModule, ChunkingContext, ChunkingType, MinifyType,
31        ModuleChunkItemIdExt,
32    },
33    ident::AssetIdent,
34    issue::IssueSource,
35    module::{Module, ModuleSideEffects},
36    module_graph::ModuleGraph,
37    reference::{ModuleReference, ModuleReferences},
38    reference_type::EcmaScriptModulesReferenceSubType,
39    resolve::{
40        BindingUsage, ExportUsage, ModuleResolveResult, ResolveErrorMode, origin::ResolveOrigin,
41        parse::Request,
42    },
43};
44use turbopack_resolve::ecmascript::esm_resolve;
45
46use crate::{
47    EcmascriptChunkPlaceable,
48    analyzer::JsValue,
49    chunk::{EcmascriptChunkItemContent, EcmascriptExports, ecmascript_chunk_item},
50    code_gen::{CodeGen, CodeGeneration, IntoCodeGenReference},
51    create_visitor,
52    references::{
53        AstPath,
54        pattern_mapping::{PatternMapping, ResolveType},
55    },
56    runtime_functions::{TURBOPACK_EXPORT_VALUE, TURBOPACK_REQUIRE},
57    utils::module_id_to_lit,
58};
59
60// ---------------------------------------------------------------------------
61// Options parsing
62// ---------------------------------------------------------------------------
63
64/// Parsed options from an `import.meta.glob(patterns, options?)` call.
65#[derive(Debug, Clone)]
66pub struct ImportMetaGlobOptions {
67    /// One or more glob patterns (first argument).
68    pub patterns: Vec<RcStr>,
69    /// When `true`, modules are loaded synchronously (eager mode).
70    pub eager: bool,
71    /// Named export to select (e.g., `"default"`, `"setup"`).
72    pub import: Option<RcStr>,
73    /// Query string to append to every import request (e.g., `"?raw"`).
74    pub query: Option<RcStr>,
75    /// Base path for resolving and keying modules.
76    pub base: Option<RcStr>,
77}
78
79/// Parse the arguments of an `import.meta.glob(patterns, options?)` call.
80///
81/// `args[0]` must be a string literal or an array of string literals.
82/// `args[1]` (optional) must be an object literal with known keys.
83///
84/// ## Unsupported Vite features
85///
86/// - **`import.meta.globEager()`** (removed in Vite 3) is not recognized. Users should migrate to
87///   `import.meta.glob('...', { eager: true })`.
88/// - **`as` option** (deprecated in Vite 5 in favor of `query`) is not supported. Use `query:
89///   '?raw'` or `query: '?url'` instead.
90pub fn parse_import_meta_glob(
91    args: &[JsValue],
92    handler: &Handler,
93    span: Span,
94    diagnostic_id: DiagnosticId,
95) -> Option<ImportMetaGlobOptions> {
96    if args.is_empty() || args.len() > 2 {
97        handler.span_warn_with_code(
98            span,
99            "import.meta.glob() requires 1 or 2 arguments",
100            diagnostic_id,
101        );
102        return None;
103    }
104
105    // --- Parse patterns (first argument) ---
106    let patterns = {
107        let mut pats = Vec::new();
108        match &args[0] {
109            JsValue::Array { items, .. } => {
110                for item in items {
111                    if let Some(s) = item.as_str() {
112                        pats.push(s.into());
113                    } else {
114                        handler.span_warn_with_code(
115                            span,
116                            "import.meta.glob() pattern array elements must be constant strings",
117                            diagnostic_id,
118                        );
119                        return None;
120                    }
121                }
122                if pats.is_empty() {
123                    handler.span_warn_with_code(
124                        span,
125                        "import.meta.glob() requires at least one pattern",
126                        diagnostic_id,
127                    );
128                    return None;
129                }
130            }
131            _ => {
132                if let Some(s) = args[0].as_str() {
133                    pats.push(s.into());
134                } else {
135                    handler.span_warn_with_code(
136                        span,
137                        "import.meta.glob() first argument must be a string literal or array of \
138                         string literals",
139                        diagnostic_id,
140                    );
141                    return None;
142                }
143            }
144        }
145        pats
146    };
147
148    // --- Parse options (second argument, optional) ---
149    let mut eager = false;
150    let mut import = None;
151    let mut query = None;
152    let mut base = None;
153
154    if let Some(opts) = args.get(1) {
155        match opts {
156            JsValue::Object { parts, .. } => {
157                use crate::analyzer::ObjectPart;
158                for part in parts {
159                    if let ObjectPart::KeyValue(key, val) = part {
160                        match key.as_str() {
161                            Some("eager") => {
162                                if let Some(b) = val.as_bool() {
163                                    eager = b;
164                                } else {
165                                    handler.span_warn_with_code(
166                                        span,
167                                        "import.meta.glob() 'eager' option must be a constant \
168                                         boolean (true or false), defaulting to false",
169                                        diagnostic_id.clone(),
170                                    );
171                                }
172                            }
173                            Some("import") => {
174                                if let Some(s) = val.as_str() {
175                                    // `import: '*'` means namespace import (whole module),
176                                    // which is the default behavior — no need to store it.
177                                    if s != "*" {
178                                        import = Some(s.into());
179                                    }
180                                } else {
181                                    handler.span_warn_with_code(
182                                        span,
183                                        "import.meta.glob() 'import' option must be a constant \
184                                         string, ignoring",
185                                        diagnostic_id.clone(),
186                                    );
187                                }
188                            }
189                            Some("query") => {
190                                if let Some(s) = val.as_str() {
191                                    // Ensure query starts with '?'
192                                    let q: RcStr = if s.starts_with('?') {
193                                        s.into()
194                                    } else {
195                                        format!("?{s}").into()
196                                    };
197                                    query = Some(q);
198                                } else if let JsValue::Object { parts, .. } = val {
199                                    // Support object form: { query: { bar: 'foo', raw: true } }
200                                    // Serializes to "?bar=foo&raw=true" with URL-encoding.
201                                    use crate::analyzer::ObjectPart;
202                                    let mut pairs: Vec<String> = Vec::new();
203                                    for part in parts {
204                                        if let ObjectPart::KeyValue(k, v) = part {
205                                            if let Some(k_str) = k.as_str() {
206                                                let enc_key = urlencoding::encode(k_str);
207                                                if let Some(v_str) = v.as_str() {
208                                                    let enc_val = urlencoding::encode(v_str);
209                                                    pairs.push(format!("{enc_key}={enc_val}"));
210                                                } else if let Some(v_bool) = v.as_bool() {
211                                                    pairs.push(format!("{enc_key}={v_bool}"));
212                                                } else {
213                                                    handler.span_warn_with_code(
214                                                        span,
215                                                        &format!(
216                                                            "import.meta.glob() 'query' object \
217                                                             value for key '{k_str}' must be a \
218                                                             constant string or boolean, ignoring"
219                                                        ),
220                                                        diagnostic_id.clone(),
221                                                    );
222                                                }
223                                            } else {
224                                                handler.span_warn_with_code(
225                                                    span,
226                                                    "import.meta.glob() 'query' object keys must \
227                                                     be constant strings",
228                                                    diagnostic_id.clone(),
229                                                );
230                                            }
231                                        } else {
232                                            handler.span_warn_with_code(
233                                                span,
234                                                "import.meta.glob() 'query' object must only \
235                                                 contain constant key-value pairs",
236                                                diagnostic_id.clone(),
237                                            );
238                                        }
239                                    }
240                                    if !pairs.is_empty() {
241                                        query = Some(format!("?{}", pairs.join("&")).into());
242                                    }
243                                } else {
244                                    handler.span_warn_with_code(
245                                        span,
246                                        "import.meta.glob() 'query' option must be a constant \
247                                         string, ignoring",
248                                        diagnostic_id.clone(),
249                                    );
250                                }
251                            }
252                            Some("base") => {
253                                if let Some(s) = val.as_str() {
254                                    base = Some(s.into());
255                                } else {
256                                    handler.span_warn_with_code(
257                                        span,
258                                        "import.meta.glob() 'base' option must be a constant \
259                                         string, ignoring",
260                                        diagnostic_id.clone(),
261                                    );
262                                }
263                            }
264                            // The `as` option was deprecated in Vite 5 in favor of `query`.
265                            // We don't support it; users should use `query` instead.
266                            Some("as") => {
267                                handler.span_warn_with_code(
268                                    span,
269                                    "import.meta.glob() 'as' option is not supported. Use 'query' \
270                                     instead (e.g. { query: '?raw' })",
271                                    diagnostic_id.clone(),
272                                );
273                            }
274                            Some(other) => {
275                                handler.span_warn_with_code(
276                                    span,
277                                    &format!(
278                                        "import.meta.glob() unsupported option '{other}'. \
279                                         Supported options are: eager, import, query, base"
280                                    ),
281                                    diagnostic_id.clone(),
282                                );
283                            }
284                            None => {
285                                handler.span_warn_with_code(
286                                    span,
287                                    "import.meta.glob() option keys must be constant strings",
288                                    diagnostic_id.clone(),
289                                );
290                            }
291                        }
292                    }
293                }
294            }
295            _ => {
296                handler.span_err_with_code(
297                    span,
298                    "import.meta.glob() second argument must be an object literal",
299                    diagnostic_id.clone(),
300                );
301                return None;
302            }
303        }
304    }
305
306    Some(ImportMetaGlobOptions {
307        patterns,
308        eager,
309        import,
310        query,
311        base,
312    })
313}
314
315// ---------------------------------------------------------------------------
316// Helpers for collecting files from ReadGlobResult
317// ---------------------------------------------------------------------------
318
319/// Strip the `./` prefix from a Vite-style glob pattern to produce a pattern
320/// compatible with Turbopack's `Glob` (which operates relative to the scan
321/// directory, without a leading `./`).
322fn strip_relative_prefix(pattern: &str) -> &str {
323    pattern.strip_prefix("./").unwrap_or(pattern)
324}
325
326/// Flatten a nested `ReadGlobResult` into a sorted list of
327/// `(base_relative_path, FileSystemPath)` pairs.
328///
329/// `ReadGlobResult` stores results in a tree of `HashMap`s keyed by path
330/// segment. This function walks the tree and collects all file entries with
331/// their full relative paths (relative to the directory `read_glob` was called
332/// on).
333async fn flatten_read_glob(result: &ReadGlobResult) -> Result<Vec<(RcStr, FileSystemPath)>> {
334    let mut files = Vec::new();
335
336    // Collect file entries from the current node.
337    fn collect_files(
338        node: &ReadGlobResult,
339        prefix: &str,
340        files: &mut Vec<(RcStr, FileSystemPath)>,
341    ) {
342        for (segment, entry) in &node.results {
343            let full_path = if prefix.is_empty() {
344                segment.to_string()
345            } else {
346                format!("{prefix}/{segment}")
347            };
348            if let DirectoryEntry::File(path) = entry {
349                files.push((full_path.into(), path.clone()));
350            }
351        }
352    }
353
354    // Walk the tree level by level, resolving Vc references as we go.
355    let mut pending: Vec<(String, turbo_tasks::ReadRef<ReadGlobResult>)> = Vec::new();
356    collect_files(result, "", &mut files);
357
358    // Resolve child directories (skip dot-directories like .git, .next, etc.)
359    for (segment, inner_vc) in &result.inner {
360        let child_prefix = segment.to_string();
361        let inner = inner_vc.await?;
362        pending.push((child_prefix, inner));
363    }
364
365    while let Some((prefix, node)) = pending.pop() {
366        collect_files(&node, &prefix, &mut files);
367        for (segment, inner_vc) in &node.inner {
368            let child_prefix = format!("{prefix}/{segment}");
369            let inner = inner_vc.await?;
370            pending.push((child_prefix, inner));
371        }
372    }
373
374    files.sort_by(|a: &(RcStr, _), b: &(RcStr, _)| a.0.cmp(&b.0));
375    Ok(files)
376}
377
378// ---------------------------------------------------------------------------
379// ImportMetaGlobMap — the resolved file map
380// ---------------------------------------------------------------------------
381
382#[turbo_tasks::value]
383#[derive(Debug)]
384pub struct ImportMetaGlobMapEntry {
385    /// Path relative to origin (the calling file's directory), used for import
386    /// resolution and as the key in the generated JS object.
387    pub origin_relative: RcStr,
388    pub request: ResolvedVc<Request>,
389    pub result: ResolvedVc<ModuleResolveResult>,
390}
391
392#[turbo_tasks::value(transparent)]
393pub struct ImportMetaGlobMap(
394    #[bincode(with = "turbo_bincode::indexmap")] FxIndexMap<RcStr, ImportMetaGlobMapEntry>,
395);
396
397#[turbo_tasks::value_impl]
398impl ImportMetaGlobMap {
399    /// Discover files matching glob patterns and resolve them as ESM imports.
400    ///
401    /// `base_dir` is the directory to scan (origin dir, or origin + base).
402    /// `positive_glob` is a `Glob` matching the wanted files (relative to
403    /// base_dir). `negative_glob` optionally excludes files. Both globs
404    /// operate on paths *relative to base_dir*.
405    #[turbo_tasks::function]
406    pub(crate) async fn generate(
407        origin: Vc<Box<dyn ResolveOrigin>>,
408        base_dir: FileSystemPath,
409        positive_glob: Vc<Glob>,
410        negative_glob: Option<Vc<Glob>>,
411        query: Option<RcStr>,
412        eager: bool,
413        issue_source: Option<IssueSource>,
414        error_mode: ResolveErrorMode,
415    ) -> Result<Vc<Self>> {
416        let origin_path = origin.origin_path().await?.parent();
417
418        // Use read_glob for efficient directory-pruning file discovery.
419        let glob_result = base_dir.read_glob(positive_glob).await?;
420        let files = flatten_read_glob(&glob_result).await?;
421
422        // Pre-resolve the negative glob (if any) once, outside the loop.
423        let negative = if let Some(neg) = negative_glob {
424            Some(neg.await?)
425        } else {
426            None
427        };
428
429        let reference_sub_type = if eager {
430            EcmaScriptModulesReferenceSubType::Import
431        } else {
432            EcmaScriptModulesReferenceSubType::DynamicImport
433        };
434
435        // Resolve all matched files in parallel.
436        let entries: Vec<_> = files
437            .iter()
438            .filter(|(base_relative, _)| {
439                // Apply negative pattern filtering on the base-relative path.
440                if let Some(ref neg) = negative {
441                    !neg.matches(base_relative)
442                } else {
443                    true
444                }
445            })
446            .map(|(_base_relative, path)| {
447                let origin_path = &origin_path;
448                let query = &query;
449                let reference_sub_type = &reference_sub_type;
450                async move {
451                    // Compute the origin-relative path for import resolution and as the
452                    // user-visible key in the result object.
453                    let Some(origin_relative) = origin_path.get_relative_path_to(path) else {
454                        bail!(
455                            "import.meta.glob: failed to compute relative path from origin to \
456                             matched file"
457                        );
458                    };
459
460                    // Append query string if specified (e.g., `?raw`).
461                    let request_str: RcStr = if let Some(q) = query {
462                        format!("{origin_relative}{q}").into()
463                    } else {
464                        origin_relative.clone()
465                    };
466
467                    let request = Request::parse_string(request_str).to_resolved().await?;
468
469                    let result = esm_resolve(
470                        origin,
471                        *request,
472                        reference_sub_type.clone(),
473                        error_mode,
474                        issue_source,
475                    )
476                    .await?
477                    .to_resolved()
478                    .await?;
479
480                    Ok((
481                        origin_relative.clone(),
482                        ImportMetaGlobMapEntry {
483                            origin_relative,
484                            request,
485                            result,
486                        },
487                    ))
488                }
489            })
490            .try_join()
491            .await?;
492
493        let mut map: FxIndexMap<RcStr, ImportMetaGlobMapEntry> = entries.into_iter().collect();
494
495        map.sort_keys();
496
497        Ok(Vc::cell(map))
498    }
499}
500
501// ---------------------------------------------------------------------------
502// ImportMetaGlobModuleReference — per-file reference from the virtual module
503// ---------------------------------------------------------------------------
504
505/// A reference from the `ImportMetaGlobAsset` virtual module to one of the
506/// glob-matched modules. Carries `ExportUsage` so that tree shaking can
507/// narrow the used exports when the `import` option is set (e.g. `{ import:
508/// 'default' }` means only the `default` export is needed).
509#[turbo_tasks::value]
510#[derive(ValueToString)]
511#[value_to_string("import.meta.glob resolved reference")]
512pub struct ImportMetaGlobModuleReference {
513    result: ResolvedVc<ModuleResolveResult>,
514    export: ExportUsage,
515}
516
517#[turbo_tasks::value_impl]
518impl ModuleReference for ImportMetaGlobModuleReference {
519    #[turbo_tasks::function]
520    fn resolve_reference(&self) -> Vc<ModuleResolveResult> {
521        *self.result
522    }
523
524    fn chunking_type(&self) -> Option<ChunkingType> {
525        Some(ChunkingType::Parallel {
526            inherit_async: false,
527            hoisted: false,
528        })
529    }
530
531    fn binding_usage(&self) -> BindingUsage {
532        BindingUsage {
533            import: Default::default(),
534            export: self.export.clone(),
535        }
536    }
537}
538
539// ---------------------------------------------------------------------------
540// ImportMetaGlobAsset — the virtual module
541// ---------------------------------------------------------------------------
542
543/// Build the unique modifier string for an `ImportMetaGlobAsset` ident.
544///
545/// Every option that affects the generated module content must be included so
546/// that two `import.meta.glob()` calls with different options get different
547/// module idents (and therefore different entries in the module graph).
548fn modifier(
549    patterns: &[RcStr],
550    eager: bool,
551    import: &Option<RcStr>,
552    query: &Option<RcStr>,
553    base: &Option<RcStr>,
554) -> RcStr {
555    let mut s = format!("import.meta.glob {}", patterns.join(", "));
556    if eager {
557        s.push_str(" eager");
558    }
559    if let Some(named) = import {
560        s.push_str(" import=");
561        s.push_str(named);
562    }
563    if let Some(q) = query {
564        s.push_str(" query=");
565        s.push_str(q);
566    }
567    if let Some(b) = base {
568        s.push_str(" base=");
569        s.push_str(b);
570    }
571    s.into()
572}
573
574#[turbo_tasks::value]
575pub struct ImportMetaGlobAsset {
576    pub origin: ResolvedVc<Box<dyn ResolveOrigin>>,
577    pub patterns: Vec<RcStr>,
578    pub eager: bool,
579    pub import: Option<RcStr>,
580    pub query: Option<RcStr>,
581    pub base: Option<RcStr>,
582    pub issue_source: Option<IssueSource>,
583    pub error_mode: ResolveErrorMode,
584}
585
586#[turbo_tasks::value_impl]
587impl ImportMetaGlobAsset {
588    /// Compute and cache the resolved file map for this glob.
589    ///
590    /// Builds the positive and negative `Glob` matchers from `self.patterns`,
591    /// scans the filesystem via `read_glob`, and resolves each matched file as
592    /// an ESM import.  Being a `#[turbo_tasks::function]`, the result is
593    /// memoised — repeated calls with the same inputs return the cached map.
594    #[turbo_tasks::function]
595    pub async fn map(&self) -> Result<Vc<ImportMetaGlobMap>> {
596        let origin = *self.origin;
597        let origin_dir = origin.origin_path().await?.parent();
598
599        // Compute the base directory for glob scanning.
600        // With `base`, patterns are resolved relative to origin + base.
601        let base_dir = if let Some(ref b) = self.base {
602            origin_dir.join(b)?
603        } else {
604            origin_dir
605        };
606
607        // Separate positive (matching) and negative (exclusion) patterns.
608        // Negative patterns start with `!`; the `!` prefix is stripped.
609        let (positive_raw, negative_raw): (Vec<_>, Vec<_>) =
610            self.patterns.iter().partition(|p| !p.starts_with('!'));
611
612        // Build the positive Glob. Turbopack's Glob operates on paths relative
613        // to the scan directory (no leading `./`), so strip that prefix. For
614        // multiple patterns, use `Glob::alternatives` to combine them.
615        let positive_globs: Vec<Vc<Glob>> = positive_raw
616            .iter()
617            .map(|p| Glob::new(strip_relative_prefix(p).into(), GlobOptions::default()))
618            .collect();
619
620        let positive_glob = if positive_globs.len() == 1 {
621            positive_globs.into_iter().next().unwrap()
622        } else {
623            Glob::alternatives(positive_globs)
624        };
625
626        // Build the negative Glob (if any). Negative patterns also need `./`
627        // stripped and are combined into a single alternation glob.
628        let negative_glob = if !negative_raw.is_empty() {
629            let neg_globs: Vec<Vc<Glob>> = negative_raw
630                .iter()
631                .map(|p| {
632                    let stripped = p.strip_prefix('!').unwrap_or(p);
633                    let stripped = strip_relative_prefix(stripped);
634                    Glob::new(stripped.into(), GlobOptions::default())
635                })
636                .collect();
637
638            let neg = if neg_globs.len() == 1 {
639                neg_globs.into_iter().next().unwrap()
640            } else {
641                Glob::alternatives(neg_globs)
642            };
643            Some(neg)
644        } else {
645            None
646        };
647
648        Ok(ImportMetaGlobMap::generate(
649            origin,
650            base_dir,
651            positive_glob,
652            negative_glob,
653            self.query.clone(),
654            self.eager,
655            self.issue_source,
656            self.error_mode,
657        ))
658    }
659}
660
661#[turbo_tasks::value_impl]
662impl Module for ImportMetaGlobAsset {
663    #[turbo_tasks::function]
664    async fn ident(&self) -> Result<Vc<AssetIdent>> {
665        let origin_path = self.origin.origin_path().owned().await?;
666        Ok(AssetIdent::from_path(origin_path)
667            .with_modifier(modifier(
668                &self.patterns,
669                self.eager,
670                &self.import,
671                &self.query,
672                &self.base,
673            ))
674            .into_vc())
675    }
676
677    #[turbo_tasks::function]
678    fn source(&self) -> Vc<turbopack_core::source::OptionSource> {
679        Vc::cell(None)
680    }
681
682    #[turbo_tasks::function]
683    async fn references(self: Vc<Self>) -> Result<Vc<ModuleReferences>> {
684        let this = self.await?;
685        let map = &*self.map().await?;
686
687        let export = match &this.import {
688            Some(name) => ExportUsage::Named(name.clone()),
689            None => ExportUsage::All,
690        };
691
692        Ok(Vc::cell(
693            map.iter()
694                .map(|(_, entry)| {
695                    ResolvedVc::upcast(
696                        ImportMetaGlobModuleReference {
697                            result: entry.result,
698                            export: export.clone(),
699                        }
700                        .resolved_cell(),
701                    )
702                })
703                .collect(),
704        ))
705    }
706
707    #[turbo_tasks::function]
708    fn side_effects(&self) -> Vc<ModuleSideEffects> {
709        if self.eager {
710            // In eager mode the module's imports are evaluated synchronously, so
711            // the module evaluation itself is side-effect-free but its imports
712            // are not necessarily.
713            ModuleSideEffects::ModuleEvaluationIsSideEffectFree.cell()
714        } else {
715            // In lazy mode the virtual module only exports thunks; no imports
716            // are evaluated, so it is fully side-effect-free.
717            ModuleSideEffects::SideEffectFree.cell()
718        }
719    }
720}
721
722#[turbo_tasks::value_impl]
723impl ChunkableModule for ImportMetaGlobAsset {
724    #[turbo_tasks::function]
725    fn as_chunk_item(
726        self: ResolvedVc<Self>,
727        module_graph: ResolvedVc<ModuleGraph>,
728        chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
729    ) -> Vc<Box<dyn turbopack_core::chunk::ChunkItem>> {
730        ecmascript_chunk_item(ResolvedVc::upcast(self), module_graph, chunking_context)
731    }
732}
733
734#[turbo_tasks::value_impl]
735impl EcmascriptChunkPlaceable for ImportMetaGlobAsset {
736    #[turbo_tasks::function]
737    fn get_exports(&self) -> Vc<EcmascriptExports> {
738        EcmascriptExports::Value.cell()
739    }
740
741    #[turbo_tasks::function]
742    async fn chunk_item_content(
743        self: Vc<Self>,
744        chunking_context: Vc<Box<dyn ChunkingContext>>,
745        _module_graph: Vc<ModuleGraph>,
746        _async_module_info: Option<Vc<AsyncModuleInfo>>,
747        _estimated: bool,
748    ) -> Result<Vc<EcmascriptChunkItemContent>> {
749        let this = self.await?;
750        let map = &*self.map().await?;
751        let minify = chunking_context.minify_type().await?;
752
753        let mut glob_map = ObjectLit {
754            span: DUMMY_SP,
755            props: vec![],
756        };
757
758        for (key, entry) in map {
759            let pm = PatternMapping::resolve_request(
760                *entry.request,
761                *this.origin,
762                chunking_context,
763                *entry.result,
764                ResolveType::ChunkItem,
765            )
766            .await?;
767
768            let PatternMapping::Single(pm) = &*pm else {
769                continue;
770            };
771
772            let key_expr = Expr::Lit(Lit::Str(entry.origin_relative.as_str().into()));
773
774            // Generate the value expression based on eager/lazy and import options
775            let value_expr = if this.eager {
776                // Eager: direct synchronous require
777                let module_expr = pm.create_require(Cow::Borrowed(&key_expr));
778                // If `import` option is set, access the named export
779                if let Some(named) = &this.import {
780                    quote!(
781                        "$module[$named]" as Expr,
782                        module: Expr = module_expr,
783                        named: Expr = Expr::Lit(Lit::Str(named.as_str().into()))
784                    )
785                } else {
786                    module_expr
787                }
788            } else {
789                // Lazy: thunk returning a Promise
790                let import_expr = pm.create_import(Cow::Borrowed(&key_expr), false);
791                if let Some(named) = &this.import {
792                    // Wrap the promise with .then(m => m[named])
793                    quote!(
794                        "() => $promise.then((m) => m[$named])" as Expr,
795                        promise: Expr = import_expr,
796                        named: Expr = Expr::Lit(Lit::Str(named.as_str().into()))
797                    )
798                } else {
799                    quote!(
800                        "() => $promise" as Expr,
801                        promise: Expr = import_expr
802                    )
803                }
804            };
805
806            // Use the origin-relative path as the key — this is what Vite does
807            // and what the user sees in `Object.keys(modules)`.
808            let prop = KeyValueProp {
809                key: PropName::Str(key.as_str().into()),
810                value: Box::new(value_expr),
811            };
812
813            glob_map
814                .props
815                .push(PropOrSpread::Prop(Box::new(Prop::KeyValue(prop))));
816        }
817
818        let expr = quote_expr!(
819            "$turbopack_export_value($obj);",
820            turbopack_export_value: Expr = TURBOPACK_EXPORT_VALUE.into(),
821            obj: Expr = Expr::Object(glob_map),
822        );
823
824        let module = ast::Module {
825            span: DUMMY_SP,
826            body: vec![ModuleItem::Stmt(Stmt::Expr(ExprStmt {
827                span: DUMMY_SP,
828                expr,
829            }))],
830            shebang: None,
831        };
832
833        let source_map: Arc<swc_core::common::SourceMap> = Default::default();
834
835        let mut bytes: Vec<u8> = vec![];
836        let mut wr: JsWriter<'_, &mut Vec<u8>> =
837            JsWriter::new(source_map.clone(), "\n", &mut bytes, None);
838        if matches!(*minify, MinifyType::Minify { .. }) {
839            wr.set_indent_str("");
840        }
841
842        let mut emitter = Emitter {
843            cfg: swc_core::ecma::codegen::Config::default(),
844            cm: source_map.clone(),
845            comments: None,
846            wr,
847        };
848
849        emitter.emit_module(&module)?;
850
851        Ok(EcmascriptChunkItemContent {
852            inner_code: bytes.into(),
853            ..Default::default()
854        }
855        .cell())
856    }
857}
858
859// ---------------------------------------------------------------------------
860// ImportMetaGlobAssetReference — the call-site reference
861// ---------------------------------------------------------------------------
862
863#[turbo_tasks::value]
864#[derive(Hash, Debug, ValueToString)]
865pub struct ImportMetaGlobAssetReference {
866    pub inner: ResolvedVc<ImportMetaGlobAsset>,
867    pub patterns: Vec<RcStr>,
868}
869
870impl std::fmt::Display for ImportMetaGlobAssetReference {
871    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
872        write!(f, "import.meta.glob {}", self.patterns.join(", "))
873    }
874}
875
876impl ImportMetaGlobAssetReference {
877    pub fn new(
878        origin: ResolvedVc<Box<dyn ResolveOrigin>>,
879        patterns: Vec<RcStr>,
880        eager: bool,
881        import: Option<RcStr>,
882        query: Option<RcStr>,
883        base: Option<RcStr>,
884        issue_source: Option<IssueSource>,
885        error_mode: ResolveErrorMode,
886    ) -> Self {
887        let inner = ImportMetaGlobAsset {
888            origin,
889            patterns: patterns.clone(),
890            eager,
891            import,
892            query,
893            base,
894            issue_source,
895            error_mode,
896        }
897        .resolved_cell();
898
899        ImportMetaGlobAssetReference { inner, patterns }
900    }
901}
902
903#[turbo_tasks::value_impl]
904impl ModuleReference for ImportMetaGlobAssetReference {
905    #[turbo_tasks::function]
906    fn resolve_reference(&self) -> Vc<ModuleResolveResult> {
907        *ModuleResolveResult::module(ResolvedVc::upcast(self.inner))
908    }
909
910    fn chunking_type(&self) -> Option<ChunkingType> {
911        Some(ChunkingType::Parallel {
912            inherit_async: false,
913            hoisted: false,
914        })
915    }
916}
917
918impl IntoCodeGenReference for ImportMetaGlobAssetReference {
919    fn into_code_gen_reference(
920        self,
921        path: AstPath,
922    ) -> (ResolvedVc<Box<dyn ModuleReference>>, CodeGen) {
923        let reference = self.resolved_cell();
924        (
925            ResolvedVc::upcast(reference),
926            CodeGen::ImportMetaGlobAssetReferenceCodeGen(ImportMetaGlobAssetReferenceCodeGen {
927                reference,
928                path,
929            }),
930        )
931    }
932}
933
934// ---------------------------------------------------------------------------
935// ImportMetaGlobAssetReferenceCodeGen — AST rewriting
936// ---------------------------------------------------------------------------
937
938#[derive(
939    PartialEq, Eq, TraceRawVcs, ValueDebugFormat, NonLocalValue, Hash, Debug, Encode, Decode,
940)]
941pub struct ImportMetaGlobAssetReferenceCodeGen {
942    path: AstPath,
943    reference: ResolvedVc<ImportMetaGlobAssetReference>,
944}
945
946impl ImportMetaGlobAssetReferenceCodeGen {
947    pub async fn code_generation(
948        &self,
949        chunking_context: Vc<Box<dyn ChunkingContext>>,
950    ) -> Result<CodeGeneration> {
951        let module_id = self
952            .reference
953            .await?
954            .inner
955            .chunk_item_id(chunking_context)
956            .await?;
957
958        let mut visitors = Vec::new();
959        visitors.push(create_visitor!(
960            self.path,
961            visit_mut_expr,
962            |expr: &mut Expr| {
963                if let Expr::Call(_) = expr {
964                    // Replace import.meta.glob(...) with __turbopack_require__(<virtual_module_id>)
965                    *expr = quote!(
966                        "$turbopack_require($id)" as Expr,
967                        turbopack_require: Expr = TURBOPACK_REQUIRE.into(),
968                        id: Expr = module_id_to_lit(&module_id)
969                    );
970                }
971            }
972        ));
973        Ok(CodeGeneration::visitors(visitors))
974    }
975}