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