Skip to main content

next_core/
util.rs

1use std::{borrow::Cow, fmt::Display, str::FromStr};
2
3use anyhow::{Result, bail};
4use bincode::{Decode, Encode};
5use next_taskless::{expand_next_js_template, expand_next_js_template_no_imports};
6use serde::{Deserialize, de::DeserializeOwned};
7use turbo_rcstr::{RcStr, rcstr};
8use turbo_tasks::{FxIndexMap, NonLocalValue, Vc, fxindexset, trace::TraceRawVcs, turbobail};
9use turbo_tasks_fs::{File, FileContent, FileJsonContent, FileSystem, FileSystemPath, rope::Rope};
10use turbopack::module_options::RuleCondition;
11use turbopack_core::{
12    asset::AssetContent,
13    compile_time_info::{
14        CompileTimeDefineValue, CompileTimeDefines, DefinableNameSegment, FreeVarReference,
15        FreeVarReferences,
16    },
17    condition::ContextCondition,
18    issue::IssueSeverity,
19    source::Source,
20    virtual_source::VirtualSource,
21};
22
23use crate::{
24    embed_js::next_js_fs, next_config::NextConfig, next_import_map::get_next_package,
25    next_manifests::ProxyMatcher, next_shared::webpack_rules::WebpackLoaderBuiltinCondition,
26};
27
28const NEXT_TEMPLATE_PATH: &str = "dist/esm/build/templates";
29
30/// As opposed to [`EnvMap`], this map allows for `None` values, which means that the variables
31/// should be replace with undefined.
32#[turbo_tasks::value(transparent)]
33pub struct OptionEnvMap(
34    #[turbo_tasks(trace_ignore)]
35    #[bincode(with = "turbo_bincode::indexmap")]
36    FxIndexMap<RcStr, Option<RcStr>>,
37);
38
39pub fn defines(define_env: &FxIndexMap<RcStr, Option<RcStr>>) -> CompileTimeDefines {
40    let mut defines = FxIndexMap::default();
41
42    for (k, v) in define_env {
43        defines
44            .entry(
45                k.split('.')
46                    .map(|s| DefinableNameSegment::Name(s.into()))
47                    .collect::<Vec<_>>(),
48            )
49            .or_insert_with(|| {
50                if let Some(v) = v {
51                    let val = serde_json::Value::from_str(v);
52                    match val {
53                        Ok(v) => v.into(),
54                        _ => CompileTimeDefineValue::Evaluate(v.clone()),
55                    }
56                } else {
57                    CompileTimeDefineValue::Undefined
58                }
59            });
60    }
61
62    CompileTimeDefines(defines)
63}
64
65/// Emits warnings or errors when inlining frequently changing Vercel system env vars
66pub fn free_var_references_with_vercel_system_env_warnings(
67    defines: CompileTimeDefines,
68    severity: IssueSeverity,
69) -> FreeVarReferences {
70    // List of system env vars:
71    //   not available as NEXT_PUBLIC_* anyway:
72    //      CI
73    //      VERCEL
74    //      VERCEL_SKEW_PROTECTION_ENABLED
75    //      VERCEL_AUTOMATION_BYPASS_SECRET
76    //      VERCEL_GIT_PROVIDER
77    //      VERCEL_GIT_REPO_SLUG
78    //      VERCEL_GIT_REPO_OWNER
79    //      VERCEL_GIT_REPO_ID
80    //      VERCEL_OIDC_TOKEN
81    //
82    //   constant:
83    //      VERCEL_PROJECT_PRODUCTION_URL
84    //      VERCEL_REGION
85    //      VERCEL_PROJECT_ID
86    //
87    //   suboptimal (changes production main branch VS preview branches):
88    //      VERCEL_ENV
89    //      VERCEL_TARGET_ENV
90    //
91    //   bad (changes per branch):
92    //      VERCEL_BRANCH_URL
93    //      VERCEL_GIT_COMMIT_REF
94    //      VERCEL_GIT_PULL_REQUEST_ID
95    //
96    //   catastrophic (changes per commit):
97    //      NEXT_DEPLOYMENT_ID
98    //      VERCEL_URL
99    //      VERCEL_DEPLOYMENT_ID
100    //      VERCEL_GIT_COMMIT_SHA
101    //      VERCEL_GIT_COMMIT_MESSAGE
102    //      VERCEL_GIT_COMMIT_AUTHOR_LOGIN
103    //      VERCEL_GIT_COMMIT_AUTHOR_NAME
104    //      VERCEL_GIT_PREVIOUS_SHA
105
106    let entries = defines
107        .0
108        .into_iter()
109        .map(|(k, value)| (k, FreeVarReference::Value(value)));
110
111    fn wrap_report_next_public_usage(
112        public_env_var: &str,
113        inner: Option<Box<FreeVarReference>>,
114        severity: IssueSeverity,
115    ) -> FreeVarReference {
116        let message = match public_env_var {
117            "NEXT_PUBLIC_NEXT_DEPLOYMENT_ID" | "NEXT_PUBLIC_VERCEL_DEPLOYMENT_ID" => {
118                rcstr!(
119                    "The deployment id is being inlined.\nThis variable changes frequently, \
120                     causing slower deploy times and worse browser client-side caching. Use \
121                     `process.env.NEXT_DEPLOYMENT_ID` instead to access the same value without \
122                     inlining, for faster deploy times and better browser client-side caching."
123                )
124            }
125            "NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA" => {
126                rcstr!(
127                    "The commit hash is being inlined.\nThis variable changes frequently, causing \
128                     slower deploy times and worse browser client-side caching. Consider using \
129                     `process.env.NEXT_DEPLOYMENT_ID` to identify a deployment. Alternatively, \
130                     use `process.env.VERCEL_GIT_COMMIT_SHA` in server side code and for browser \
131                     code, remove it."
132                )
133            }
134            "NEXT_PUBLIC_VERCEL_BRANCH_URL" | "NEXT_PUBLIC_VERCEL_URL" => format!(
135                "The deployment url system environment variable is being inlined.\nThis variable \
136                 changes frequently, causing slower deploy times and worse browser client-side \
137                 caching. For server-side code, replace with `process.env.{}` and for browser \
138                 code, read `location.host` instead.",
139                public_env_var.strip_prefix("NEXT_PUBLIC_").unwrap(),
140            )
141            .into(),
142            _ => format!(
143                "A system environment variable is being inlined.\nThis variable changes \
144                 frequently, causing slower deploy times and worse browser client-side caching. \
145                 For server-side code, replace with `process.env.{}` and for browser code, try to \
146                 remove it.",
147                public_env_var.strip_prefix("NEXT_PUBLIC_").unwrap(),
148            )
149            .into(),
150        };
151        FreeVarReference::ReportUsage {
152            message,
153            severity,
154            inner,
155        }
156    }
157
158    let mut list = fxindexset!(
159        "NEXT_PUBLIC_NEXT_DEPLOYMENT_ID",
160        "NEXT_PUBLIC_VERCEL_BRANCH_URL",
161        "NEXT_PUBLIC_VERCEL_DEPLOYMENT_ID",
162        "NEXT_PUBLIC_VERCEL_GIT_COMMIT_AUTHOR_LOGIN",
163        "NEXT_PUBLIC_VERCEL_GIT_COMMIT_AUTHOR_NAME",
164        "NEXT_PUBLIC_VERCEL_GIT_COMMIT_MESSAGE",
165        "NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF",
166        "NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA",
167        "NEXT_PUBLIC_VERCEL_GIT_PREVIOUS_SHA",
168        "NEXT_PUBLIC_VERCEL_GIT_PULL_REQUEST_ID",
169        "NEXT_PUBLIC_VERCEL_URL",
170    );
171
172    let mut entries: FxIndexMap<_, _> = entries
173        .map(|(k, value)| {
174            let value = if let &[
175                DefinableNameSegment::Name(a),
176                DefinableNameSegment::Name(b),
177                DefinableNameSegment::Name(public_env_var),
178            ] = &&*k
179                && a == "process"
180                && b == "env"
181                && list.swap_remove(&**public_env_var)
182            {
183                wrap_report_next_public_usage(public_env_var, Some(Box::new(value)), severity)
184            } else {
185                value
186            };
187            (k, value)
188        })
189        .collect();
190
191    // For the remaining ones, still add a warning, but without replacement
192    for public_env_var in list {
193        entries.insert(
194            vec![
195                rcstr!("process").into(),
196                rcstr!("env").into(),
197                DefinableNameSegment::Name(public_env_var.into()),
198            ],
199            wrap_report_next_public_usage(public_env_var, None, severity),
200        );
201    }
202
203    FreeVarReferences(entries)
204}
205
206#[turbo_tasks::task_input]
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TraceRawVcs, Encode, Decode)]
208pub enum PathType {
209    PagesPage,
210    PagesApi,
211    Data,
212}
213
214/// Converts a filename within the server root into a next pathname.
215#[turbo_tasks::function]
216pub async fn pathname_for_path(
217    server_root: FileSystemPath,
218    server_path: FileSystemPath,
219    path_ty: PathType,
220) -> Result<Vc<RcStr>> {
221    let server_path_value = server_path.clone();
222    let path = if let Some(path) = server_root.get_path_to(&server_path_value) {
223        path
224    } else {
225        turbobail!("server_path ({server_path}) is not in server_root ({server_root})");
226    };
227    let path = match (path_ty, path) {
228        // "/" is special-cased to "/index" for data routes.
229        (PathType::Data, "") => rcstr!("/index"),
230        // `get_path_to` always strips the leading `/` from the path, so we need to add
231        // it back here.
232        (_, path) => format!("/{path}").into(),
233    };
234
235    Ok(Vc::cell(path))
236}
237
238// Adapted from https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/get-asset-path-from-route.ts
239// TODO(alexkirsz) There's no need to create an intermediate string here (and
240// below), we should instead return an `impl Display`.
241pub fn get_asset_prefix_from_pathname(pathname: &str) -> String {
242    if pathname == "/" {
243        "/index".to_string()
244    } else if pathname == "/index" || pathname.starts_with("/index/") {
245        format!("/index{pathname}")
246    } else {
247        pathname.to_string()
248    }
249}
250
251// Adapted from https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/get-asset-path-from-route.ts
252pub fn get_asset_path_from_pathname(pathname: &str, ext: &str) -> String {
253    format!("{}{}", get_asset_prefix_from_pathname(pathname), ext)
254}
255
256#[turbo_tasks::function]
257pub async fn get_transpiled_packages(
258    next_config: Vc<NextConfig>,
259    project_path: FileSystemPath,
260) -> Result<Vc<Vec<RcStr>>> {
261    let mut transpile_packages: Vec<RcStr> = next_config.transpile_packages().owned().await?;
262
263    let default_transpiled_packages: Vec<RcStr> = load_next_js_json_file(
264        project_path,
265        rcstr!("dist/lib/default-transpiled-packages.json"),
266    )
267    .await?;
268
269    transpile_packages.extend(default_transpiled_packages.iter().cloned());
270
271    Ok(Vc::cell(transpile_packages))
272}
273
274pub async fn foreign_code_context_condition(
275    next_config: Vc<NextConfig>,
276    project_path: FileSystemPath,
277) -> Result<ContextCondition> {
278    let transpiled_packages = get_transpiled_packages(next_config, project_path.clone()).await?;
279
280    // The next template files are allowed to import the user's code via import
281    // mapping, and imports must use the project-level [ResolveOptions] instead
282    // of the `node_modules` specific resolve options (the template files are
283    // technically node module files).
284    let not_next_template_dir = ContextCondition::not(ContextCondition::InPath(
285        get_next_package(project_path.clone())
286            .await?
287            .join(NEXT_TEMPLATE_PATH)?,
288    ));
289
290    let result = ContextCondition::all(vec![
291        ContextCondition::InNodeModules,
292        not_next_template_dir,
293        ContextCondition::not(ContextCondition::any(
294            transpiled_packages
295                .iter()
296                .map(|package| ContextCondition::InDirectory(format!("node_modules/{package}")))
297                .collect(),
298        )),
299    ]);
300    Ok(result)
301}
302
303/// Determines if the module is an internal asset (i.e overlay, fallback) coming from the embedded
304/// FS, don't apply user defined transforms.
305//
306// TODO: Turbopack specific embed fs paths should be handled by internals of Turbopack itself and
307// user config should not try to leak this. However, currently we apply few transform options
308// subject to Next.js's configuration even if it's embedded assets.
309pub async fn internal_assets_conditions() -> Result<ContextCondition> {
310    Ok(ContextCondition::any(vec![
311        ContextCondition::InPath(next_js_fs().root().owned().await?),
312        ContextCondition::InPath(
313            turbopack_ecmascript_runtime::embed_fs()
314                .root()
315                .owned()
316                .await?,
317        ),
318        ContextCondition::InPath(turbopack_node::embed_js::embed_fs().root().owned().await?),
319        ContextCondition::InPath(
320            turbopack_ecmascript::embed_js::embed_fs()
321                .root()
322                .owned()
323                .await?,
324        ),
325        ContextCondition::InPath(turbopack_wasm::embed::embed_fs().root().owned().await?),
326    ]))
327}
328
329pub fn app_function_name(page: impl Display) -> String {
330    format!("app{page}")
331}
332pub fn pages_function_name(page: impl Display) -> String {
333    format!("pages{page}")
334}
335
336#[turbo_tasks::task_input]
337#[derive(
338    Default,
339    PartialEq,
340    Eq,
341    Clone,
342    Copy,
343    Debug,
344    TraceRawVcs,
345    Deserialize,
346    Hash,
347    PartialOrd,
348    Ord,
349    Encode,
350    Decode,
351)]
352#[serde(rename_all = "lowercase")]
353pub enum NextRuntime {
354    #[default]
355    NodeJs,
356    #[serde(alias = "experimental-edge")]
357    Edge,
358}
359
360impl NextRuntime {
361    /// Returns conditions that can be used in the Next.js config's turbopack "rules" section for
362    /// defining webpack loader configuration.
363    pub fn webpack_loader_conditions(&self) -> impl Iterator<Item = WebpackLoaderBuiltinCondition> {
364        match self {
365            NextRuntime::NodeJs => [WebpackLoaderBuiltinCondition::Node],
366            NextRuntime::Edge => [WebpackLoaderBuiltinCondition::EdgeLight],
367        }
368        .into_iter()
369    }
370
371    /// Returns conditions used by `ResolveOptionsContext`.
372    pub fn custom_resolve_conditions(&self) -> impl Iterator<Item = RcStr> {
373        match self {
374            NextRuntime::NodeJs => [rcstr!("node")],
375            NextRuntime::Edge => [rcstr!("edge-light")],
376        }
377        .into_iter()
378    }
379}
380
381#[derive(PartialEq, Eq, Clone, Debug, TraceRawVcs, NonLocalValue, Encode, Decode)]
382pub enum MiddlewareMatcherKind {
383    Str(String),
384    Matcher(ProxyMatcher),
385}
386
387/// Loads a next.js template, replaces `replacements` and `injections` and makes
388/// sure there are none left over.
389pub async fn load_next_js_template<'b>(
390    template_path: &'b str,
391    project_path: FileSystemPath,
392    replacements: impl IntoIterator<Item = (&'b str, &'b str)>,
393    injections: impl IntoIterator<Item = (&'b str, &'b str)>,
394    imports: impl IntoIterator<Item = (&'b str, Option<&'b str>)>,
395) -> Result<Vc<Box<dyn Source>>> {
396    let template_path = virtual_next_js_template_path(project_path.clone(), template_path).await?;
397
398    let content = file_content_rope(template_path.read()).await?;
399    let content = content.to_str()?;
400
401    let package_root = get_next_package(project_path).await?;
402
403    let content = expand_next_js_template(
404        &content,
405        &template_path.path,
406        &package_root.path,
407        replacements,
408        injections,
409        imports,
410    )?;
411
412    let file = File::from(content);
413    let source = VirtualSource::new(
414        template_path,
415        AssetContent::file(FileContent::Content(file).cell()),
416    );
417
418    Ok(Vc::upcast(source))
419}
420
421/// Loads a next.js template but does **not** require that any relative imports are present
422/// or rewritten. This is intended for small internal templates that do not have their own
423/// imports but still use template variables/injections.
424pub async fn load_next_js_template_no_imports(
425    template_path: &str,
426    project_path: FileSystemPath,
427    replacements: &[(&str, &str)],
428    injections: &[(&str, &str)],
429    imports: &[(&str, Option<&str>)],
430) -> Result<Vc<Box<dyn Source>>> {
431    let template_path = virtual_next_js_template_path(project_path.clone(), template_path).await?;
432
433    let content = file_content_rope(template_path.read()).await?;
434    let content = content.to_str()?;
435
436    let package_root = get_next_package(project_path).await?;
437
438    let content = expand_next_js_template_no_imports(
439        &content,
440        &template_path.path,
441        &package_root.path,
442        replacements.iter().copied(),
443        injections.iter().copied(),
444        imports.iter().copied(),
445    )?;
446
447    let file = File::from(content);
448    let source = VirtualSource::new(
449        template_path,
450        AssetContent::file(FileContent::Content(file).cell()),
451    );
452
453    Ok(Vc::upcast(source))
454}
455
456#[turbo_tasks::function]
457pub async fn file_content_rope(content: Vc<FileContent>) -> Result<Vc<Rope>> {
458    let content = &*content.await?;
459
460    let FileContent::Content(file) = content else {
461        bail!("Expected file content for file");
462    };
463
464    Ok(file.content().to_owned().cell())
465}
466
467async fn virtual_next_js_template_path(
468    project_path: FileSystemPath,
469    file: &str,
470) -> Result<FileSystemPath> {
471    debug_assert!(!file.contains('/'));
472    get_next_package(project_path)
473        .await?
474        .join(&format!("{NEXT_TEMPLATE_PATH}/{file}"))
475}
476
477pub async fn load_next_js_json_file<T: DeserializeOwned>(
478    project_path: FileSystemPath,
479    sub_path: RcStr,
480) -> Result<T> {
481    let file_path = get_next_package(project_path.clone())
482        .await?
483        .join(&sub_path)?;
484
485    let content = &*file_path.read().await?;
486
487    match content.parse_json_ref() {
488        FileJsonContent::Unparsable(e) => bail!("File is not valid JSON: {e}"),
489        FileJsonContent::NotFound => turbobail!("File not found: {file_path:?}",),
490        FileJsonContent::Content(value) => Ok(serde_json::from_value(value)?),
491    }
492}
493
494pub async fn load_next_js_jsonc_file<T: DeserializeOwned>(
495    project_path: FileSystemPath,
496    sub_path: RcStr,
497) -> Result<T> {
498    let file_path = get_next_package(project_path.clone())
499        .await?
500        .join(&sub_path)?;
501
502    let content = &*file_path.read().await?;
503
504    match content.parse_json_with_comments_ref() {
505        FileJsonContent::Unparsable(e) => turbobail!("File is not valid JSON: {e}"),
506        FileJsonContent::NotFound => turbobail!("File not found: {file_path}",),
507        FileJsonContent::Content(value) => Ok(serde_json::from_value(value)?),
508    }
509}
510
511pub fn styles_rule_condition() -> RuleCondition {
512    RuleCondition::any(vec![
513        RuleCondition::all(vec![
514            RuleCondition::ResourcePathEndsWith(".css".into()),
515            RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.css".into())),
516        ]),
517        RuleCondition::all(vec![
518            RuleCondition::ResourcePathEndsWith(".sass".into()),
519            RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.sass".into())),
520        ]),
521        RuleCondition::all(vec![
522            RuleCondition::ResourcePathEndsWith(".scss".into()),
523            RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.scss".into())),
524        ]),
525        RuleCondition::all(vec![
526            RuleCondition::ContentTypeStartsWith("text/css".into()),
527            RuleCondition::not(RuleCondition::ContentTypeStartsWith(
528                "text/css+module".into(),
529            )),
530        ]),
531        RuleCondition::all(vec![
532            RuleCondition::ContentTypeStartsWith("text/sass".into()),
533            RuleCondition::not(RuleCondition::ContentTypeStartsWith(
534                "text/sass+module".into(),
535            )),
536        ]),
537        RuleCondition::all(vec![
538            RuleCondition::ContentTypeStartsWith("text/scss".into()),
539            RuleCondition::not(RuleCondition::ContentTypeStartsWith(
540                "text/scss+module".into(),
541            )),
542        ]),
543    ])
544}
545pub fn module_styles_rule_condition() -> RuleCondition {
546    RuleCondition::any(vec![
547        RuleCondition::ResourcePathEndsWith(".module.css".into()),
548        RuleCondition::ResourcePathEndsWith(".module.scss".into()),
549        RuleCondition::ResourcePathEndsWith(".module.sass".into()),
550        RuleCondition::ContentTypeStartsWith("text/css+module".into()),
551        RuleCondition::ContentTypeStartsWith("text/sass+module".into()),
552        RuleCondition::ContentTypeStartsWith("text/scss+module".into()),
553    ])
554}
555
556/// Returns the list of global variables that should be forwarded from the main
557/// context to web workers. These are Next.js-specific globals that need to be
558/// available in worker contexts.
559pub fn worker_forwarded_globals() -> Vec<RcStr> {
560    vec![
561        rcstr!("NEXT_DEPLOYMENT_ID"),
562        rcstr!("NEXT_CLIENT_ASSET_SUFFIX"),
563    ]
564}
565
566/// The globs defined in the next.config.mjs are relative to the project root.
567/// The glob walker in turbopack is somewhat naive so we handle relative path directives first so
568/// traversal doesn't need to consider them and can just traverse 'down' the tree.
569/// The main alternative is to merge glob evaluation with directory traversal which is what the npm
570/// `glob` package does, but this would be a substantial rewrite.
571pub fn relativize_glob<'a>(
572    glob: &'a str,
573    relative_to: &FileSystemPath,
574) -> Result<(&'a str, FileSystemPath)> {
575    let mut relative_to = Cow::Borrowed(relative_to);
576    let mut processed_glob = glob;
577    loop {
578        if let Some(stripped) = processed_glob.strip_prefix("../") {
579            if relative_to.path.is_empty() {
580                bail!(
581                    "glob '{glob}' is invalid, it has a prefix that navigates out of the project \
582                     root"
583                );
584            }
585            relative_to = Cow::Owned(relative_to.parent());
586            processed_glob = stripped;
587        } else if let Some(stripped) = processed_glob.strip_prefix("./") {
588            processed_glob = stripped;
589        } else {
590            break;
591        }
592    }
593    Ok((processed_glob, relative_to.into_owned()))
594}
595
596#[cfg(test)]
597mod tests {
598    use turbo_tasks::ResolvedVc;
599    use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
600    use turbo_tasks_fs::{FileSystemPath, NullFileSystem};
601
602    use super::*;
603
604    fn create_test_fs_path(path: &str) -> FileSystemPath {
605        FileSystemPath {
606            fs: ResolvedVc::upcast(NullFileSystem {}.resolved_cell()),
607            path: path.into(),
608        }
609    }
610
611    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
612    async fn test_relativize_glob_normal_patterns() {
613        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
614            BackendOptions::default(),
615            noop_backing_storage(),
616        ));
617        tt.run_once(async {
618            // Test normal glob patterns without relative prefixes
619            let base_path = create_test_fs_path("project/src");
620
621            let (glob, path) = relativize_glob("*.js", &base_path).unwrap();
622            assert_eq!(glob, "*.js");
623            assert_eq!(path.path.as_str(), "project/src");
624
625            let (glob, path) = relativize_glob("components/**/*.tsx", &base_path).unwrap();
626            assert_eq!(glob, "components/**/*.tsx");
627            assert_eq!(path.path.as_str(), "project/src");
628
629            let (glob, path) = relativize_glob("lib/utils.ts", &base_path).unwrap();
630            assert_eq!(glob, "lib/utils.ts");
631            assert_eq!(path.path.as_str(), "project/src");
632            Ok(())
633        })
634        .await
635        .unwrap();
636    }
637
638    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
639    async fn test_relativize_glob_current_directory_prefix() {
640        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
641            BackendOptions::default(),
642            noop_backing_storage(),
643        ));
644        tt.run_once(async {
645            let base_path = create_test_fs_path("project/src");
646
647            // Single ./ prefix
648            let (glob, path) = relativize_glob("./components/*.tsx", &base_path).unwrap();
649            assert_eq!(glob, "components/*.tsx");
650            assert_eq!(path.path.as_str(), "project/src");
651
652            // Multiple ./ prefixes
653            let (glob, path) = relativize_glob("././utils.js", &base_path).unwrap();
654            assert_eq!(glob, "utils.js");
655            assert_eq!(path.path.as_str(), "project/src");
656
657            // ./ with complex glob
658            let (glob, path) = relativize_glob("./lib/**/*.{js,ts}", &base_path).unwrap();
659            assert_eq!(glob, "lib/**/*.{js,ts}");
660            assert_eq!(path.path.as_str(), "project/src");
661            Ok(())
662        })
663        .await
664        .unwrap();
665    }
666
667    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
668    async fn test_relativize_glob_parent_directory_navigation() {
669        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
670            BackendOptions::default(),
671            noop_backing_storage(),
672        ));
673        tt.run_once(async {
674            let base_path = create_test_fs_path("project/src/components");
675
676            // Single ../ prefix
677            let (glob, path) = relativize_glob("../utils/*.js", &base_path).unwrap();
678            assert_eq!(glob, "utils/*.js");
679            assert_eq!(path.path.as_str(), "project/src");
680
681            // Multiple ../ prefixes
682            let (glob, path) = relativize_glob("../../lib/*.ts", &base_path).unwrap();
683            assert_eq!(glob, "lib/*.ts");
684            assert_eq!(path.path.as_str(), "project");
685
686            // Complex navigation with glob
687            let (glob, path) = relativize_glob("../../../external/**/*.json", &base_path).unwrap();
688            assert_eq!(glob, "external/**/*.json");
689            assert_eq!(path.path.as_str(), "");
690            Ok(())
691        })
692        .await
693        .unwrap();
694    }
695
696    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
697    async fn test_relativize_glob_mixed_prefixes() {
698        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
699            BackendOptions::default(),
700            noop_backing_storage(),
701        ));
702        tt.run_once(async {
703            let base_path = create_test_fs_path("project/src/components");
704
705            // ../ followed by ./
706            let (glob, path) = relativize_glob(".././utils/*.js", &base_path).unwrap();
707            assert_eq!(glob, "utils/*.js");
708            assert_eq!(path.path.as_str(), "project/src");
709
710            // ./ followed by ../
711            let (glob, path) = relativize_glob("./../lib/*.ts", &base_path).unwrap();
712            assert_eq!(glob, "lib/*.ts");
713            assert_eq!(path.path.as_str(), "project/src");
714
715            // Multiple mixed prefixes
716            let (glob, path) = relativize_glob("././../.././external/*.json", &base_path).unwrap();
717            assert_eq!(glob, "external/*.json");
718            assert_eq!(path.path.as_str(), "project");
719            Ok(())
720        })
721        .await
722        .unwrap();
723    }
724
725    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
726    async fn test_relativize_glob_error_navigation_out_of_root() {
727        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
728            BackendOptions::default(),
729            noop_backing_storage(),
730        ));
731        tt.run_once(async {
732            // Test navigating out of project root with empty path
733            let empty_path = create_test_fs_path("");
734            let result = relativize_glob("../outside.js", &empty_path);
735            assert!(result.is_err());
736            assert!(
737                result
738                    .unwrap_err()
739                    .to_string()
740                    .contains("navigates out of the project root")
741            );
742
743            // Test navigating too far up from a shallow path
744            let shallow_path = create_test_fs_path("project");
745            let result = relativize_glob("../../outside.js", &shallow_path);
746            assert!(result.is_err());
747            assert!(
748                result
749                    .unwrap_err()
750                    .to_string()
751                    .contains("navigates out of the project root")
752            );
753
754            // Test multiple ../ that would go out of root
755            let base_path = create_test_fs_path("a/b");
756            let result = relativize_glob("../../../outside.js", &base_path);
757            assert!(result.is_err());
758            assert!(
759                result
760                    .unwrap_err()
761                    .to_string()
762                    .contains("navigates out of the project root")
763            );
764            Ok(())
765        })
766        .await
767        .unwrap();
768    }
769}