Skip to main content

turbopack_ecmascript/references/
import_meta_glob.rs

1use std::{borrow::Cow, sync::Arc};
2
3use anyhow::{Context as _, 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,
23};
24use turbo_tasks_fs::{
25    DirectoryEntry, FileSystemPath, ReadGlobResult,
26    glob::{Glob, GlobOptions, relativize_glob},
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    ast_path_trie::{AstPathId, AstPathTrie, AstPathTrieBuilder},
50    chunk::{EcmascriptChunkItemContent, EcmascriptExports, ecmascript_chunk_item},
51    code_gen::{CodeGen, CodeGeneration, IntoCodeGenReference},
52    create_visitor,
53    references::pattern_mapping::{PatternMapping, ResolveType},
54    runtime_functions::{TURBOPACK_EXPORT_VALUE, TURBOPACK_REQUIRE},
55    utils::module_id_to_lit,
56};
57
58// ---------------------------------------------------------------------------
59// Options parsing
60// ---------------------------------------------------------------------------
61
62/// Parsed options from an `import.meta.glob(patterns, options?)` call.
63#[derive(Debug, Clone)]
64pub struct ImportMetaGlobOptions {
65    /// One or more glob patterns (first argument).
66    pub patterns: Vec<RcStr>,
67    /// When `true`, modules are loaded synchronously (eager mode).
68    pub eager: bool,
69    /// Named export to select (e.g., `"default"`, `"setup"`).
70    pub import: Option<RcStr>,
71    /// Query string to append to every import request (e.g., `"?raw"`).
72    pub query: Option<RcStr>,
73    /// Base path for resolving and keying modules.
74    pub base: Option<RcStr>,
75    /// Whether glob matching is case-sensitive.
76    pub case_sensitive: bool,
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    let mut case_sensitive = true;
154
155    if let Some(opts) = args.get(1) {
156        match opts {
157            JsValue::Object { parts, .. } => {
158                use crate::analyzer::ObjectPart;
159                for part in parts {
160                    if let ObjectPart::KeyValue(key, val) = part {
161                        match key.as_str() {
162                            Some("eager") => {
163                                if let Some(b) = val.as_bool() {
164                                    eager = b;
165                                } else {
166                                    handler.span_warn_with_code(
167                                        span,
168                                        "import.meta.glob() 'eager' option must be a constant \
169                                         boolean (true or false), defaulting to false",
170                                        diagnostic_id.clone(),
171                                    );
172                                }
173                            }
174                            Some("import") => {
175                                if let Some(s) = val.as_str() {
176                                    // `import: '*'` means namespace import (whole module),
177                                    // which is the default behavior — no need to store it.
178                                    if s != "*" {
179                                        import = Some(s.into());
180                                    }
181                                } else {
182                                    handler.span_warn_with_code(
183                                        span,
184                                        "import.meta.glob() 'import' option must be a constant \
185                                         string, ignoring",
186                                        diagnostic_id.clone(),
187                                    );
188                                }
189                            }
190                            Some("query") => {
191                                if let Some(s) = val.as_str() {
192                                    // Ensure query starts with '?'
193                                    let q: RcStr = if s.starts_with('?') {
194                                        s.into()
195                                    } else {
196                                        format!("?{s}").into()
197                                    };
198                                    query = Some(q);
199                                } else if let JsValue::Object { parts, .. } = val {
200                                    // Support object form: { query: { bar: 'foo', raw: true } }
201                                    // Serializes to "?bar=foo&raw=true" with URL-encoding.
202                                    use crate::analyzer::ObjectPart;
203                                    let mut pairs: Vec<String> = Vec::new();
204                                    for part in parts {
205                                        if let ObjectPart::KeyValue(k, v) = part {
206                                            if let Some(k_str) = k.as_str() {
207                                                let enc_key = urlencoding::encode(k_str);
208                                                if let Some(v_str) = v.as_str() {
209                                                    let enc_val = urlencoding::encode(v_str);
210                                                    pairs.push(format!("{enc_key}={enc_val}"));
211                                                } else if let Some(v_bool) = v.as_bool() {
212                                                    pairs.push(format!("{enc_key}={v_bool}"));
213                                                } else {
214                                                    handler.span_warn_with_code(
215                                                        span,
216                                                        &format!(
217                                                            "import.meta.glob() 'query' object \
218                                                             value for key '{k_str}' must be a \
219                                                             constant string or boolean, ignoring"
220                                                        ),
221                                                        diagnostic_id.clone(),
222                                                    );
223                                                }
224                                            } else {
225                                                handler.span_warn_with_code(
226                                                    span,
227                                                    "import.meta.glob() 'query' object keys must \
228                                                     be constant strings",
229                                                    diagnostic_id.clone(),
230                                                );
231                                            }
232                                        } else {
233                                            handler.span_warn_with_code(
234                                                span,
235                                                "import.meta.glob() 'query' object must only \
236                                                 contain constant key-value pairs",
237                                                diagnostic_id.clone(),
238                                            );
239                                        }
240                                    }
241                                    if !pairs.is_empty() {
242                                        query = Some(format!("?{}", pairs.join("&")).into());
243                                    }
244                                } else {
245                                    handler.span_warn_with_code(
246                                        span,
247                                        "import.meta.glob() 'query' option must be a constant \
248                                         string, ignoring",
249                                        diagnostic_id.clone(),
250                                    );
251                                }
252                            }
253                            Some("base") => {
254                                if let Some(s) = val.as_str() {
255                                    base = Some(s.into());
256                                } else {
257                                    handler.span_warn_with_code(
258                                        span,
259                                        "import.meta.glob() 'base' option must be a constant \
260                                         string, ignoring",
261                                        diagnostic_id.clone(),
262                                    );
263                                }
264                            }
265                            Some("caseSensitive") => {
266                                if let Some(b) = val.as_bool() {
267                                    case_sensitive = b;
268                                } else {
269                                    handler.span_warn_with_code(
270                                        span,
271                                        "import.meta.glob() 'caseSensitive' option must be a \
272                                         constant boolean (true or false), defaulting to true",
273                                        diagnostic_id.clone(),
274                                    );
275                                }
276                            }
277                            // The `as` option was deprecated in Vite 5 in favor of `query`.
278                            // We don't support it; users should use `query` instead.
279                            Some("as") => {
280                                handler.span_warn_with_code(
281                                    span,
282                                    "import.meta.glob() 'as' option is not supported. Use 'query' \
283                                     instead (e.g. { query: '?raw' })",
284                                    diagnostic_id.clone(),
285                                );
286                            }
287                            Some(other) => {
288                                handler.span_warn_with_code(
289                                    span,
290                                    &format!(
291                                        "import.meta.glob() unsupported option '{other}'. \
292                                         Supported options are: eager, import, query, base, \
293                                         caseSensitive"
294                                    ),
295                                    diagnostic_id.clone(),
296                                );
297                            }
298                            None => {
299                                handler.span_warn_with_code(
300                                    span,
301                                    "import.meta.glob() option keys must be constant strings",
302                                    diagnostic_id.clone(),
303                                );
304                            }
305                        }
306                    }
307                }
308            }
309            _ => {
310                handler.span_err_with_code(
311                    span,
312                    "import.meta.glob() second argument must be an object literal",
313                    diagnostic_id.clone(),
314                );
315                return None;
316            }
317        }
318    }
319
320    Some(ImportMetaGlobOptions {
321        patterns,
322        eager,
323        import,
324        query,
325        base,
326        case_sensitive,
327    })
328}
329
330// ---------------------------------------------------------------------------
331// Pattern normalization
332// ---------------------------------------------------------------------------
333
334/// Where a single Vite-style glob pattern is rooted.
335#[derive(Debug, Clone, Copy, PartialEq, Eq)]
336enum PatternRoot {
337    /// The pattern is absolute from the project root (it started with `/`).
338    ProjectRoot,
339    /// The pattern is relative to the importing file.
340    Relative,
341}
342
343/// Split a Vite-style glob pattern into the directory it is rooted in and the
344/// remaining glob, which only traverses down from there.
345///
346/// Vite's rule: a pattern is either relative to the importing file (`./`, `../`)
347/// or absolute from the project root (`/`).
348/// <https://vite.dev/guide/features.html#glob-import-caveats>
349///
350/// The leading `/` is the part specific to `import.meta.glob`; resolving `./` and
351/// `../` against a directory is the general filesystem operation shared with
352/// other glob options, so that part is delegated to [`relativize_glob`].
353///
354/// `root` is the directory a `/`-rooted pattern resolves from, the same one a
355/// `/`-rooted request resolves from. It is [`None`] when such requests aren't
356/// supported, in which case callers must have rejected `/`-rooted patterns
357/// before getting here.
358///
359/// Returns [`None`] if the pattern walks above `root`.
360fn split_pattern<'a>(
361    pattern: &'a str,
362    base_dir: &FileSystemPath,
363    root: Option<&FileSystemPath>,
364) -> Option<(PatternRoot, &'a str, FileSystemPath)> {
365    if let Some(rest) = pattern.strip_prefix('/') {
366        return Some((PatternRoot::ProjectRoot, rest, root?.clone()));
367    }
368    let (rest, dir) = relativize_glob(pattern, base_dir)?;
369    Some((PatternRoot::Relative, rest, dir))
370}
371
372/// Strip the `!` prefix that marks a negative (exclusion) pattern.
373fn strip_negation(pattern: &str) -> &str {
374    pattern.strip_prefix('!').unwrap_or(pattern)
375}
376
377/// A pattern of an `import.meta.glob()` call, split into where it is rooted and
378/// the glob relative to that root. Splitting happens once per pattern, up front,
379/// so the rest of the code can work from the result.
380struct SplitPattern<'a> {
381    /// Negative (exclusion) patterns start with `!` and only remove matches.
382    negative: bool,
383    /// Where the pattern is rooted.
384    root: PatternRoot,
385    /// The pattern, relative to `root_dir`.
386    rest: &'a str,
387    /// The directory the pattern is rooted in.
388    root_dir: FileSystemPath,
389}
390
391// ---------------------------------------------------------------------------
392// Helpers for collecting files from ReadGlobResult
393// ---------------------------------------------------------------------------
394
395/// Flatten a nested `ReadGlobResult` into a sorted list of
396/// `(base_relative_path, FileSystemPath)` pairs.
397///
398/// `ReadGlobResult` stores results in a tree of `HashMap`s keyed by path
399/// segment. This function walks the tree and collects all file entries with
400/// their full relative paths (relative to the directory `read_glob` was called
401/// on).
402async fn flatten_read_glob(result: &ReadGlobResult) -> Result<Vec<(RcStr, FileSystemPath)>> {
403    let mut files = Vec::new();
404
405    // Collect file entries from the current node.
406    fn collect_files(
407        node: &ReadGlobResult,
408        prefix: &str,
409        files: &mut Vec<(RcStr, FileSystemPath)>,
410    ) {
411        for (segment, entry) in &node.results {
412            let full_path = if prefix.is_empty() {
413                segment.to_string()
414            } else {
415                format!("{prefix}/{segment}")
416            };
417            if let DirectoryEntry::File(path) = entry {
418                files.push((full_path.into(), path.clone()));
419            }
420        }
421    }
422
423    // Walk the tree level by level, resolving Vc references as we go.
424    let mut pending: Vec<(String, turbo_tasks::ReadRef<ReadGlobResult>)> = Vec::new();
425    collect_files(result, "", &mut files);
426
427    // Resolve child directories (skip dot-directories like .git, .next, etc.)
428    for (segment, inner_vc) in &result.inner {
429        let child_prefix = segment.to_string();
430        let inner = inner_vc.await?;
431        pending.push((child_prefix, inner));
432    }
433
434    while let Some((prefix, node)) = pending.pop() {
435        collect_files(&node, &prefix, &mut files);
436        for (segment, inner_vc) in &node.inner {
437            let child_prefix = format!("{prefix}/{segment}");
438            let inner = inner_vc.await?;
439            pending.push((child_prefix, inner));
440        }
441    }
442
443    files.sort_by(|a: &(RcStr, _), b: &(RcStr, _)| a.0.cmp(&b.0));
444    Ok(files)
445}
446
447// ---------------------------------------------------------------------------
448// ImportMetaGlobMap — the resolved file map
449// ---------------------------------------------------------------------------
450
451#[turbo_tasks::value]
452#[derive(Debug)]
453pub struct ImportMetaGlobMapEntry {
454    /// Path relative to origin (the calling file's directory), used for import
455    /// resolution.
456    pub origin_relative: RcStr,
457    pub request: ResolvedVc<Request>,
458    pub result: ResolvedVc<ModuleResolveResult>,
459}
460
461#[turbo_tasks::value(transparent)]
462pub struct ImportMetaGlobMap(
463    #[bincode(with = "turbo_bincode::indexmap")] FxIndexMap<RcStr, ImportMetaGlobMapEntry>,
464);
465
466#[turbo_tasks::value_impl]
467impl ImportMetaGlobMap {
468    /// Discover files matching glob patterns and resolve them as ESM imports.
469    ///
470    /// `scan_dir` is the directory to scan. `positive_glob` is a `Glob` matching
471    /// the wanted files (relative to `scan_dir`). `negative_glob` optionally
472    /// excludes files. Both globs operate on paths *relative to `scan_dir`*.
473    ///
474    /// The keys of the returned map are the user-visible keys of the generated
475    /// object:
476    /// - relative to `key_base` (with a `./` or `../` prefix) when the `base` option was used,
477    /// - absolute from the project root (with a `/` prefix) for files matched by a
478    ///   project-root-absolute pattern (`root_absolute_glob`),
479    /// - relative to the importing file otherwise.
480    ///
481    /// `root` is the directory a `/`-rooted pattern resolves from, and that its keys are relative
482    /// to. It is always set when `root_absolute_glob` is.
483    #[turbo_tasks::function]
484    pub(crate) async fn generate(
485        origin: Vc<Box<dyn ResolveOrigin>>,
486        scan_dir: FileSystemPath,
487        positive_glob: Vc<Glob>,
488        negative_glob: Option<Vc<Glob>>,
489        root_absolute_glob: Option<Vc<Glob>>,
490        root: Option<FileSystemPath>,
491        key_base: Option<FileSystemPath>,
492        query: Option<RcStr>,
493        eager: bool,
494        lazy_compilation: bool,
495        issue_source: Option<IssueSource>,
496        error_mode: ResolveErrorMode,
497    ) -> Result<Vc<Self>> {
498        let origin_path = origin.into_trait_ref().await?.origin_path().parent();
499
500        // Use read_glob for efficient directory-pruning file discovery.
501        let glob_result = scan_dir.read_glob(positive_glob).await?;
502        let files = flatten_read_glob(&glob_result).await?;
503
504        // Pre-resolve the globs that are matched per file (if any) once, outside the loop.
505        let negative = if let Some(neg) = negative_glob {
506            Some(neg.await?)
507        } else {
508            None
509        };
510        let root_absolute = if let Some(glob) = root_absolute_glob {
511            Some(glob.await?)
512        } else {
513            None
514        };
515
516        let reference_sub_type = if eager {
517            EcmaScriptModulesReferenceSubType::Import
518        } else if lazy_compilation {
519            // Resolves to a proxy that compiles the matched module only once its
520            // thunk is first called.
521            EcmaScriptModulesReferenceSubType::LazyDynamicImport
522        } else {
523            EcmaScriptModulesReferenceSubType::DynamicImport
524        };
525
526        // Resolve all matched files in parallel.
527        let entries: Vec<_> = files
528            .iter()
529            .filter(|(scan_relative, _)| {
530                // Apply negative pattern filtering on the scan-dir-relative path.
531                if let Some(ref neg) = negative {
532                    !neg.matches(scan_relative)
533                } else {
534                    true
535                }
536            })
537            .map(|(scan_relative, _logical_path)| {
538                let origin_path = &origin_path;
539                let scan_dir = &scan_dir;
540                let query = &query;
541                let reference_sub_type = &reference_sub_type;
542                let key_base = &key_base;
543                let root_absolute = &root_absolute;
544                let root = &root;
545                async move {
546                    // ReadGlobResult paths are logical too, but reconstruct from its keys here so
547                    // matching and user-visible specifiers have one explicit source of truth. The
548                    // module resolver resolves this logical request and tracks its symlink chain.
549                    let logical_path = scan_dir.join(scan_relative)?;
550                    let Some(origin_relative) = origin_path.get_relative_request_to(&logical_path)
551                    else {
552                        bail!(
553                            "import.meta.glob: failed to compute relative path from origin to \
554                             matched file"
555                        );
556                    };
557
558                    // Compute the user-visible key of this entry.
559                    let key: RcStr = if let Some(key_base) = key_base {
560                        // Vite keys the result relative to `base` when it is provided, and its
561                        // keys are `./`-prefixed like the origin-relative ones below.
562                        // https://vite.dev/guide/features.html#base-path
563                        let Some(key) = key_base.get_relative_request_to(&logical_path) else {
564                            bail!(
565                                "import.meta.glob: failed to compute relative path from base to \
566                                 matched file"
567                            );
568                        };
569                        key
570                    } else if root_absolute
571                        .as_ref()
572                        .is_some_and(|glob| glob.matches(scan_relative))
573                    {
574                        // Matched by a pattern that is absolute from the root of the project, so
575                        // the key is absolute from that same root.
576                        let root = root
577                            .as_ref()
578                            .context("a project-root-absolute pattern requires a root")?;
579                        let Some(relative) = root.get_relative_path_to(&logical_path) else {
580                            bail!(
581                                "import.meta.glob: failed to compute relative path from the root \
582                                 of the project to matched file"
583                            );
584                        };
585                        format!("/{relative}").into()
586                    } else {
587                        origin_relative.clone()
588                    };
589
590                    // Append query string if specified (e.g., `?raw`).
591                    let request_str: RcStr = if let Some(q) = query {
592                        format!("{origin_relative}{q}").into()
593                    } else {
594                        origin_relative.clone()
595                    };
596
597                    let request = Request::parse_string(request_str).to_resolved().await?;
598
599                    let result = esm_resolve(
600                        origin,
601                        *request,
602                        reference_sub_type.clone(),
603                        error_mode,
604                        issue_source,
605                    )
606                    .await?
607                    .to_resolved()
608                    .await?;
609
610                    Ok((
611                        key,
612                        ImportMetaGlobMapEntry {
613                            origin_relative,
614                            request,
615                            result,
616                        },
617                    ))
618                }
619            })
620            .try_join()
621            .await?;
622
623        let mut map: FxIndexMap<RcStr, ImportMetaGlobMapEntry> = entries.into_iter().collect();
624
625        map.sort_keys();
626
627        Ok(Vc::cell(map))
628    }
629}
630
631// ---------------------------------------------------------------------------
632// ImportMetaGlobModuleReference — per-file reference from the virtual module
633// ---------------------------------------------------------------------------
634
635/// A reference from the `ImportMetaGlobAsset` virtual module to one of the
636/// glob-matched modules. Carries `ExportUsage` so that tree shaking can
637/// narrow the used exports when the `import` option is set (e.g. `{ import:
638/// 'default' }` means only the `default` export is needed).
639#[turbo_tasks::value]
640#[derive(ValueToString)]
641#[value_to_string("import.meta.glob resolved reference")]
642pub struct ImportMetaGlobModuleReference {
643    result: ResolvedVc<ModuleResolveResult>,
644    export: ExportUsage,
645    eager: bool,
646}
647
648#[turbo_tasks::value_impl]
649impl ModuleReference for ImportMetaGlobModuleReference {
650    #[turbo_tasks::function]
651    fn resolve_reference(&self) -> Vc<ModuleResolveResult> {
652        *self.result
653    }
654
655    fn chunking_type(&self) -> Option<ChunkingType> {
656        // Lazy matches are loaded on demand, like the target of a dynamic `import()`.
657        Some(if self.eager {
658            ChunkingType::Parallel {
659                inherit_async: false,
660                hoisted: false,
661            }
662        } else {
663            ChunkingType::Async
664        })
665    }
666
667    fn binding_usage(&self) -> BindingUsage {
668        BindingUsage {
669            import: Default::default(),
670            export: self.export.clone(),
671        }
672    }
673}
674
675// ---------------------------------------------------------------------------
676// ImportMetaGlobAsset — the virtual module
677// ---------------------------------------------------------------------------
678
679/// Build the unique modifier string for an `ImportMetaGlobAsset` ident.
680///
681/// Every option that affects the generated module content must be included so
682/// that two `import.meta.glob()` calls with different options get different
683/// module idents (and therefore different entries in the module graph).
684fn modifier(
685    patterns: &[RcStr],
686    eager: bool,
687    lazy_compilation: bool,
688    import: &Option<RcStr>,
689    query: &Option<RcStr>,
690    base: &Option<RcStr>,
691    case_sensitive: bool,
692) -> RcStr {
693    let mut s = format!("import.meta.glob {}", patterns.join(", "));
694    if eager {
695        s.push_str(" eager");
696    }
697    if lazy_compilation {
698        s.push_str(" lazy-compilation");
699    }
700    if let Some(named) = import {
701        s.push_str(" import=");
702        s.push_str(named);
703    }
704    if let Some(q) = query {
705        s.push_str(" query=");
706        s.push_str(q);
707    }
708    if let Some(b) = base {
709        s.push_str(" base=");
710        s.push_str(b);
711    }
712    if !case_sensitive {
713        s.push_str(" case-insensitive");
714    }
715    s.into()
716}
717
718#[turbo_tasks::value]
719pub struct ImportMetaGlobAsset {
720    pub origin: ResolvedVc<Box<dyn ResolveOrigin>>,
721    pub patterns: Vec<RcStr>,
722    pub eager: bool,
723    /// Whether the matched modules are compiled only once their thunk is first
724    /// called. Never set in eager mode, where the matches are evaluated up front.
725    pub lazy_compilation: bool,
726    pub import: Option<RcStr>,
727    pub query: Option<RcStr>,
728    pub base: Option<RcStr>,
729    pub case_sensitive: bool,
730    pub issue_source: Option<IssueSource>,
731    pub error_mode: ResolveErrorMode,
732}
733
734#[turbo_tasks::value_impl]
735impl ImportMetaGlobAsset {
736    /// Compute and cache the resolved file map for this glob.
737    ///
738    /// Builds the positive and negative `Glob` matchers from `self.patterns`,
739    /// scans the filesystem via `read_glob`, and resolves each matched file as
740    /// an ESM import.  Being a `#[turbo_tasks::function]`, the result is
741    /// memoised — repeated calls with the same inputs return the cached map.
742    #[turbo_tasks::function]
743    pub async fn map(&self) -> Result<Vc<ImportMetaGlobMap>> {
744        let origin = *self.origin;
745        let origin_dir = origin.into_trait_ref().await?.origin_path().parent();
746
747        // Compute the base directory patterns are resolved against.
748        // With `base`, patterns are resolved relative to origin + base.
749        let base_dir = if let Some(ref b) = self.base {
750            match origin_dir.try_join(b) {
751                Some(base_dir) => base_dir,
752                None => {
753                    emit_escapes_root_issue(self, &origin_dir, &format!("the 'base' option {b:?}"))
754                        .await?;
755                    return Ok(Vc::cell(Default::default()));
756                }
757            }
758        } else {
759            origin_dir.clone()
760        };
761        // A `/`-rooted pattern resolves from the same directory a `/`-rooted request
762        // resolves from, so that a pattern and a plain import agree on what `/`
763        // means. That directory is not necessarily the root of the filesystem.
764        let root = self
765            .origin
766            .into_trait_ref()
767            .await?
768            .resolve_options()
769            .await?
770            .server_relative_root
771            .clone();
772
773        // Split every pattern once: this both validates it and produces everything
774        // the globs below are built from.
775        //
776        // An invalid pattern is always reported. A positive one is then skipped, so
777        // that the patterns around it still resolve — being left with fewer matches
778        // is easier to work with while editing than an empty object. A negative one
779        // can't be skipped: dropping an exclusion would include files the user
780        // asked to leave out, so nothing is matched at all instead.
781        //
782        // A `/`-rooted pattern with no directory to resolve it from is reported
783        // separately from one that walks above that directory.
784        let mut patterns = Vec::with_capacity(self.patterns.len());
785        for written in &self.patterns {
786            let negative = written.starts_with('!');
787            let stripped = strip_negation(written);
788            let Some((pattern_root, rest, root_dir)) =
789                split_pattern(stripped, &base_dir, root.as_ref())
790            else {
791                if root.is_none() && stripped.starts_with('/') {
792                    emit_absolute_unsupported_issue(self, &format!("the pattern {written:?}"))
793                        .await?;
794                } else {
795                    emit_escapes_root_issue(self, &origin_dir, &format!("the pattern {written:?}"))
796                        .await?;
797                }
798                if negative {
799                    return Ok(Vc::cell(Default::default()));
800                }
801                continue;
802            };
803            patterns.push(SplitPattern {
804                negative,
805                root: pattern_root,
806                rest,
807                root_dir,
808            });
809        }
810
811        // Pick a single directory to scan that contains every pattern root, so
812        // that one `read_glob` call covers all of them and each pattern can be
813        // rewritten relative to it. A negative pattern can't add files, but it
814        // can be rooted above the positive patterns (e.g. `['../dir/*.js',
815        // '!/dir/skip.js']`) and still has to be expressible relative to
816        // `scan_dir`, so both are considered here.
817        let mut scan_dir = base_dir.clone();
818        for pattern in &patterns {
819            // Every root is an ancestor of `base_dir` (or of the root a `/`-rooted
820            // pattern resolves from), so the shortest path is an ancestor of all.
821            if pattern.root_dir.path.len() < scan_dir.path.len() {
822                scan_dir = pattern.root_dir.clone();
823            }
824        }
825
826        let glob_options = GlobOptions {
827            case_insensitive: !self.case_sensitive,
828            ..Default::default()
829        };
830
831        // Rewrite a pattern to be relative to `scan_dir`.
832        let relative_to_scan_dir = |pattern: &SplitPattern<'_>| -> Result<RcStr> {
833            let prefix = if pattern.root_dir == scan_dir {
834                ""
835            } else {
836                scan_dir
837                    .get_path_to(&pattern.root_dir)
838                    .context("the scanned directory must contain every pattern root")?
839            };
840            Ok(if prefix.is_empty() {
841                pattern.rest.into()
842            } else {
843                format!("{prefix}/{}", pattern.rest).into()
844            })
845        };
846
847        let mut positive_globs: Vec<Vc<Glob>> = Vec::new();
848        let mut negative_globs: Vec<Vc<Glob>> = Vec::new();
849        // The patterns that are absolute from the root of the project, so that the
850        // files they match can be keyed the same way. Only positive patterns are
851        // collected: a negative pattern removes matches instead of producing them,
852        // so it never determines how a key is spelled.
853        let mut root_absolute_globs: Vec<Vc<Glob>> = Vec::new();
854        for pattern in &patterns {
855            let glob = Glob::new(relative_to_scan_dir(pattern)?, glob_options);
856            if pattern.negative {
857                negative_globs.push(glob);
858            } else {
859                if pattern.root == PatternRoot::ProjectRoot {
860                    root_absolute_globs.push(glob);
861                }
862                positive_globs.push(glob);
863            }
864        }
865
866        let positive_glob = if positive_globs.len() == 1 {
867            positive_globs.into_iter().next().unwrap()
868        } else {
869            Glob::alternatives(positive_globs)
870        };
871
872        // Negative patterns are combined into a single alternation glob.
873        let negative_glob = if negative_globs.is_empty() {
874            None
875        } else if negative_globs.len() == 1 {
876            Some(negative_globs.into_iter().next().unwrap())
877        } else {
878            Some(Glob::alternatives(negative_globs))
879        };
880
881        let root_absolute_glob = if root_absolute_globs.is_empty() {
882            None
883        } else if root_absolute_globs.len() == 1 {
884            Some(root_absolute_globs.into_iter().next().unwrap())
885        } else {
886            Some(Glob::alternatives(root_absolute_globs))
887        };
888
889        Ok(ImportMetaGlobMap::generate(
890            origin,
891            scan_dir,
892            positive_glob,
893            negative_glob,
894            root_absolute_glob,
895            root,
896            // Vite keys the result relative to `base` when it is provided.
897            self.base.is_some().then_some(base_dir),
898            self.query.clone(),
899            self.eager,
900            self.lazy_compilation,
901            self.issue_source,
902            self.error_mode,
903        ))
904    }
905}
906
907/// A `/`-rooted pattern needs a directory to resolve from, and there is none.
908async fn emit_absolute_unsupported_issue(asset: &ImportMetaGlobAsset, what: &str) -> Result<()> {
909    CodeGenerationIssue {
910        severity: IssueSeverity::Error,
911        title: StyledString::Text(rcstr!(
912            "import.meta.glob() does not support patterns absolute from the root of the project \
913             here"
914        ))
915        .resolved_cell(),
916        message: StyledString::Text(
917            format!(
918                "{what} of import.meta.glob({}) starts with `/`, but there is no directory to \
919                 resolve it from. Use a pattern relative to the importing file instead.",
920                asset
921                    .patterns
922                    .iter()
923                    .map(|p| format!("{p:?}"))
924                    .collect::<Vec<_>>()
925                    .join(", "),
926            )
927            .into(),
928        )
929        .resolved_cell(),
930        path: asset.origin.into_trait_ref().await?.origin_path(),
931        source: asset.issue_source,
932    }
933    .resolved_cell()
934    .emit();
935    Ok(())
936}
937
938/// Report a `base` or pattern of an `import.meta.glob()` call that walks above
939/// the project root.
940async fn emit_escapes_root_issue(
941    asset: &ImportMetaGlobAsset,
942    origin_dir: &FileSystemPath,
943    what: &str,
944) -> Result<()> {
945    CodeGenerationIssue {
946        severity: IssueSeverity::Error,
947        title: StyledString::Text(rcstr!(
948            "import.meta.glob() cannot look outside of the project root"
949        ))
950        .resolved_cell(),
951        message: StyledString::Text(
952            format!(
953                "{what} of import.meta.glob({}) resolves to a directory above the project root, \
954                 relative to {}. Patterns are relative to the importing file, or absolute from \
955                 the project root when they start with `/`.",
956                asset
957                    .patterns
958                    .iter()
959                    .map(|p| format!("{p:?}"))
960                    .collect::<Vec<_>>()
961                    .join(", "),
962                origin_dir.path
963            )
964            .into(),
965        )
966        .resolved_cell(),
967        path: asset.origin.into_trait_ref().await?.origin_path(),
968        source: asset.issue_source,
969    }
970    .resolved_cell()
971    .emit();
972    Ok(())
973}
974
975#[turbo_tasks::value_impl]
976impl Module for ImportMetaGlobAsset {
977    #[turbo_tasks::function]
978    async fn ident(&self) -> Result<Vc<AssetIdent>> {
979        let origin = self.origin.into_trait_ref().await?;
980        let origin_path = origin.origin_path();
981        // The layer is part of the ident so that this virtual module is distinct
982        // per layer (the same file can be processed in multiple layers), and so
983        // that import traces can collapse it into the importing module.
984        Ok(AssetIdent::from_path(origin_path)
985            .with_layer(origin.asset_context().into_trait_ref().await?.layer())
986            .with_modifier(modifier(
987                &self.patterns,
988                self.eager,
989                self.lazy_compilation,
990                &self.import,
991                &self.query,
992                &self.base,
993                self.case_sensitive,
994            ))
995            .into_vc())
996    }
997
998    #[turbo_tasks::function]
999    fn source(&self) -> Vc<turbopack_core::source::OptionSource> {
1000        Vc::cell(None)
1001    }
1002
1003    #[turbo_tasks::function]
1004    async fn references(self: Vc<Self>) -> Result<Vc<ModuleReferences>> {
1005        let this = self.await?;
1006        let map = &*self.map().await?;
1007
1008        let export = match &this.import {
1009            Some(name) => ExportUsage::Named(name.clone()),
1010            None => ExportUsage::All,
1011        };
1012
1013        // A matched file that has no module type is reported against the file
1014        // itself, which is not part of the module graph and therefore has no
1015        // import trace. Point at the call site as well, otherwise there is
1016        // nothing connecting the error to a request the user never wrote.
1017        for (key, entry) in map.iter() {
1018            if entry.result.await?.primary.iter().any(|(_, item)| {
1019                matches!(
1020                    item,
1021                    turbopack_core::resolve::ModuleResolveResultItem::Unknown(_)
1022                )
1023            }) {
1024                CodeGenerationIssue {
1025                    severity: IssueSeverity::Error,
1026                    title: StyledString::Text(rcstr!(
1027                        "import.meta.glob() matched a file that has no module type"
1028                    ))
1029                    .resolved_cell(),
1030                    message: StyledString::Text(
1031                        format!(
1032                            "import.meta.glob({}) matched {key}, which doesn't have an associated \
1033                             module type. Narrow the pattern, exclude the file with a negative \
1034                             pattern (\"!...\"), or register a loader or module type for its file \
1035                             extension.",
1036                            this.patterns
1037                                .iter()
1038                                .map(|p| format!("{p:?}"))
1039                                .collect::<Vec<_>>()
1040                                .join(", ")
1041                        )
1042                        .into(),
1043                    )
1044                    .resolved_cell(),
1045                    path: this.origin.into_trait_ref().await?.origin_path(),
1046                    source: this.issue_source,
1047                }
1048                .resolved_cell()
1049                .emit();
1050            }
1051        }
1052
1053        Ok(Vc::cell(
1054            map.iter()
1055                .map(|(_, entry)| {
1056                    ResolvedVc::upcast(
1057                        ImportMetaGlobModuleReference {
1058                            result: entry.result,
1059                            export: export.clone(),
1060                            eager: this.eager,
1061                        }
1062                        .resolved_cell(),
1063                    )
1064                })
1065                .collect(),
1066        ))
1067    }
1068
1069    #[turbo_tasks::function]
1070    fn side_effects(&self) -> Vc<ModuleSideEffects> {
1071        if self.eager {
1072            // In eager mode the module's imports are evaluated synchronously, so
1073            // the module evaluation itself is side-effect-free but its imports
1074            // are not necessarily.
1075            ModuleSideEffects::ModuleEvaluationIsSideEffectFree.cell()
1076        } else {
1077            // In lazy mode the virtual module only exports thunks; no imports
1078            // are evaluated, so it is fully side-effect-free.
1079            ModuleSideEffects::SideEffectFree.cell()
1080        }
1081    }
1082}
1083
1084#[turbo_tasks::value_impl]
1085impl ChunkableModule for ImportMetaGlobAsset {
1086    #[turbo_tasks::function]
1087    fn as_chunk_item(
1088        self: ResolvedVc<Self>,
1089        module_graph: ResolvedVc<ModuleGraph>,
1090        chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
1091    ) -> Vc<Box<dyn turbopack_core::chunk::ChunkItem>> {
1092        ecmascript_chunk_item(ResolvedVc::upcast(self), module_graph, chunking_context)
1093    }
1094}
1095
1096#[turbo_tasks::value_impl]
1097impl EcmascriptChunkPlaceable for ImportMetaGlobAsset {
1098    #[turbo_tasks::function]
1099    fn get_exports(&self) -> Vc<EcmascriptExports> {
1100        EcmascriptExports::Value.cell()
1101    }
1102
1103    #[turbo_tasks::function]
1104    async fn chunk_item_content(
1105        self: Vc<Self>,
1106        chunking_context: Vc<Box<dyn ChunkingContext>>,
1107        _module_graph: Vc<ModuleGraph>,
1108        _async_module_info: Option<Vc<AsyncModuleInfo>>,
1109        _estimated: bool,
1110    ) -> Result<Vc<EcmascriptChunkItemContent>> {
1111        let this = self.await?;
1112        let map = &*self.map().await?;
1113        let minify = chunking_context.minify_type().await?;
1114
1115        // In lazy mode each matched module is in its own async chunk group (see
1116        // `ImportMetaGlobModuleReference::chunking_type`), so it has to be loaded
1117        // through its async loader, like the target of a dynamic `import()`.
1118        let resolve_type =
1119            if !this.eager && chunking_context.chunk_loading().await?.can_split_async() {
1120                ResolveType::AsyncChunkLoader
1121            } else {
1122                ResolveType::ChunkItem
1123            };
1124
1125        let mut glob_map = ObjectLit {
1126            span: DUMMY_SP,
1127            props: vec![],
1128        };
1129
1130        for (key, entry) in map {
1131            let pm = PatternMapping::resolve_request(
1132                *entry.request,
1133                *this.origin,
1134                chunking_context,
1135                *entry.result,
1136                resolve_type,
1137                None,
1138            )
1139            .await?;
1140
1141            let PatternMapping::Single(pm) = &*pm else {
1142                continue;
1143            };
1144
1145            let key_expr = Expr::Lit(Lit::Str(entry.origin_relative.as_str().into()));
1146
1147            // Generate the value expression based on eager/lazy and import options
1148            let value_expr = if this.eager {
1149                // Eager: synchronously evaluate the module and use its ESM namespace,
1150                // matching what a static `import * as ns from "..."` would produce.
1151                let module_expr = pm.create_esm_require(Cow::Borrowed(&key_expr));
1152                // If `import` option is set, access the named export
1153                if let Some(named) = &this.import {
1154                    quote!(
1155                        "$module[$named]" as Expr,
1156                        module: Expr = module_expr,
1157                        named: Expr = Expr::Lit(Lit::Str(named.as_str().into()))
1158                    )
1159                } else {
1160                    module_expr
1161                }
1162            } else {
1163                // Lazy: thunk returning a Promise
1164                let import_expr = pm.create_import(Cow::Borrowed(&key_expr), false);
1165                if let Some(named) = &this.import {
1166                    // Wrap the promise with .then(m => m[named])
1167                    quote!(
1168                        "() => $promise.then((m) => m[$named])" as Expr,
1169                        promise: Expr = import_expr,
1170                        named: Expr = Expr::Lit(Lit::Str(named.as_str().into()))
1171                    )
1172                } else {
1173                    quote!(
1174                        "() => $promise" as Expr,
1175                        promise: Expr = import_expr
1176                    )
1177                }
1178            };
1179
1180            // Use the origin-relative path as the key — this is what Vite does
1181            // and what the user sees in `Object.keys(modules)`.
1182            let prop = KeyValueProp {
1183                key: PropName::Str(key.as_str().into()),
1184                value: Box::new(value_expr),
1185            };
1186
1187            glob_map
1188                .props
1189                .push(PropOrSpread::Prop(Box::new(Prop::KeyValue(prop))));
1190        }
1191
1192        let expr = quote_expr!(
1193            "$turbopack_export_value($obj);",
1194            turbopack_export_value: Expr = TURBOPACK_EXPORT_VALUE.into(),
1195            obj: Expr = Expr::Object(glob_map),
1196        );
1197
1198        let module = ast::Module {
1199            span: DUMMY_SP,
1200            body: vec![ModuleItem::Stmt(Stmt::Expr(ExprStmt {
1201                span: DUMMY_SP,
1202                expr,
1203            }))],
1204            shebang: None,
1205        };
1206
1207        let source_map: Arc<swc_core::common::SourceMap> = Default::default();
1208
1209        let mut bytes: Vec<u8> = vec![];
1210        let mut wr: JsWriter<'_, &mut Vec<u8>> =
1211            JsWriter::new(source_map.clone(), "\n", &mut bytes, None);
1212        if matches!(*minify, MinifyType::Minify { .. }) {
1213            wr.set_indent_str("");
1214        }
1215
1216        let mut emitter = Emitter {
1217            cfg: swc_core::ecma::codegen::Config::default(),
1218            cm: source_map.clone(),
1219            comments: None,
1220            wr,
1221        };
1222
1223        emitter.emit_module(&module)?;
1224
1225        Ok(EcmascriptChunkItemContent {
1226            inner_code: bytes.into(),
1227            ..Default::default()
1228        }
1229        .cell())
1230    }
1231}
1232
1233// ---------------------------------------------------------------------------
1234// ImportMetaGlobAssetReference — the call-site reference
1235// ---------------------------------------------------------------------------
1236
1237#[turbo_tasks::value]
1238#[derive(Hash, Debug, ValueToString)]
1239pub struct ImportMetaGlobAssetReference {
1240    pub inner: ResolvedVc<ImportMetaGlobAsset>,
1241    pub patterns: Vec<RcStr>,
1242}
1243
1244impl std::fmt::Display for ImportMetaGlobAssetReference {
1245    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1246        write!(f, "import.meta.glob {}", self.patterns.join(", "))
1247    }
1248}
1249
1250impl ImportMetaGlobAssetReference {
1251    pub fn new(
1252        origin: ResolvedVc<Box<dyn ResolveOrigin>>,
1253        patterns: Vec<RcStr>,
1254        eager: bool,
1255        lazy_compilation: bool,
1256        import: Option<RcStr>,
1257        query: Option<RcStr>,
1258        base: Option<RcStr>,
1259        case_sensitive: bool,
1260        issue_source: Option<IssueSource>,
1261        error_mode: ResolveErrorMode,
1262    ) -> Self {
1263        let inner = ImportMetaGlobAsset {
1264            origin,
1265            patterns: patterns.clone(),
1266            eager,
1267            lazy_compilation: lazy_compilation && !eager,
1268            import,
1269            query,
1270            base,
1271            case_sensitive,
1272            issue_source,
1273            error_mode,
1274        }
1275        .resolved_cell();
1276
1277        ImportMetaGlobAssetReference { inner, patterns }
1278    }
1279}
1280
1281#[turbo_tasks::value_impl]
1282impl ModuleReference for ImportMetaGlobAssetReference {
1283    #[turbo_tasks::function]
1284    fn resolve_reference(&self) -> Vc<ModuleResolveResult> {
1285        *ModuleResolveResult::module(ResolvedVc::upcast(self.inner))
1286    }
1287
1288    fn chunking_type(&self) -> Option<ChunkingType> {
1289        Some(ChunkingType::Parallel {
1290            inherit_async: false,
1291            hoisted: false,
1292        })
1293    }
1294}
1295
1296impl IntoCodeGenReference for ImportMetaGlobAssetReference {
1297    fn into_reference(self) -> ResolvedVc<Box<dyn ModuleReference>> {
1298        ResolvedVc::upcast(self.resolved_cell())
1299    }
1300
1301    fn into_code_gen_reference(
1302        self,
1303        _trie: &AstPathTrieBuilder,
1304        path: AstPathId,
1305    ) -> (ResolvedVc<Box<dyn ModuleReference>>, CodeGen) {
1306        let reference = self.resolved_cell();
1307        (
1308            ResolvedVc::upcast(reference),
1309            CodeGen::ImportMetaGlobAssetReferenceCodeGen(ImportMetaGlobAssetReferenceCodeGen {
1310                reference,
1311                path,
1312            }),
1313        )
1314    }
1315}
1316
1317// ---------------------------------------------------------------------------
1318// ImportMetaGlobAssetReferenceCodeGen — AST rewriting
1319// ---------------------------------------------------------------------------
1320
1321#[derive(PartialEq, Eq, ValueDebugFormat, NonLocalValue, Hash, Debug, Encode, Decode)]
1322pub struct ImportMetaGlobAssetReferenceCodeGen {
1323    path: AstPathId,
1324    reference: ResolvedVc<ImportMetaGlobAssetReference>,
1325}
1326
1327impl ImportMetaGlobAssetReferenceCodeGen {
1328    pub async fn code_generation(
1329        &self,
1330        trie: &AstPathTrie,
1331        chunking_context: Vc<Box<dyn ChunkingContext>>,
1332    ) -> Result<CodeGeneration> {
1333        let module_id = self
1334            .reference
1335            .await?
1336            .inner
1337            .chunk_item_id(chunking_context)
1338            .await?;
1339
1340        let mut visitors = Vec::new();
1341        visitors.push(create_visitor!(
1342            trie,
1343            self.path,
1344            visit_mut_expr,
1345            |expr: &mut Expr| {
1346                if let Expr::Call(_) = expr {
1347                    // Replace import.meta.glob(...) with __turbopack_require__(<virtual_module_id>)
1348                    *expr = quote!(
1349                        "$turbopack_require($id)" as Expr,
1350                        turbopack_require: Expr = TURBOPACK_REQUIRE.into(),
1351                        id: Expr = module_id_to_lit(&module_id)
1352                    );
1353                }
1354            }
1355        ));
1356        Ok(CodeGeneration::visitors(visitors))
1357    }
1358}