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    ]))
320}
321
322pub fn app_function_name(page: impl Display) -> String {
323    format!("app{page}")
324}
325pub fn pages_function_name(page: impl Display) -> String {
326    format!("pages{page}")
327}
328
329#[turbo_tasks::task_input]
330#[derive(
331    Default,
332    PartialEq,
333    Eq,
334    Clone,
335    Copy,
336    Debug,
337    TraceRawVcs,
338    Deserialize,
339    Hash,
340    PartialOrd,
341    Ord,
342    Encode,
343    Decode,
344)]
345#[serde(rename_all = "lowercase")]
346pub enum NextRuntime {
347    #[default]
348    NodeJs,
349    #[serde(alias = "experimental-edge")]
350    Edge,
351}
352
353impl NextRuntime {
354    /// Returns conditions that can be used in the Next.js config's turbopack "rules" section for
355    /// defining webpack loader configuration.
356    pub fn webpack_loader_conditions(&self) -> impl Iterator<Item = WebpackLoaderBuiltinCondition> {
357        match self {
358            NextRuntime::NodeJs => [WebpackLoaderBuiltinCondition::Node],
359            NextRuntime::Edge => [WebpackLoaderBuiltinCondition::EdgeLight],
360        }
361        .into_iter()
362    }
363
364    /// Returns conditions used by `ResolveOptionsContext`.
365    pub fn custom_resolve_conditions(&self) -> impl Iterator<Item = RcStr> {
366        match self {
367            NextRuntime::NodeJs => [rcstr!("node")],
368            NextRuntime::Edge => [rcstr!("edge-light")],
369        }
370        .into_iter()
371    }
372}
373
374#[derive(PartialEq, Eq, Clone, Debug, TraceRawVcs, NonLocalValue, Encode, Decode)]
375pub enum MiddlewareMatcherKind {
376    Str(String),
377    Matcher(ProxyMatcher),
378}
379
380/// Loads a next.js template, replaces `replacements` and `injections` and makes
381/// sure there are none left over.
382pub async fn load_next_js_template<'b>(
383    template_path: &'b str,
384    project_path: FileSystemPath,
385    replacements: impl IntoIterator<Item = (&'b str, &'b str)>,
386    injections: impl IntoIterator<Item = (&'b str, &'b str)>,
387    imports: impl IntoIterator<Item = (&'b str, Option<&'b str>)>,
388) -> Result<Vc<Box<dyn Source>>> {
389    let template_path = virtual_next_js_template_path(project_path.clone(), template_path).await?;
390
391    let content = file_content_rope(template_path.read()).await?;
392    let content = content.to_str()?;
393
394    let package_root = get_next_package(project_path).await?;
395
396    let content = expand_next_js_template(
397        &content,
398        &template_path.path,
399        &package_root.path,
400        replacements,
401        injections,
402        imports,
403    )?;
404
405    let file = File::from(content);
406    let source = VirtualSource::new(
407        template_path,
408        AssetContent::file(FileContent::Content(file).cell()),
409    );
410
411    Ok(Vc::upcast(source))
412}
413
414/// Loads a next.js template but does **not** require that any relative imports are present
415/// or rewritten. This is intended for small internal templates that do not have their own
416/// imports but still use template variables/injections.
417pub async fn load_next_js_template_no_imports(
418    template_path: &str,
419    project_path: FileSystemPath,
420    replacements: &[(&str, &str)],
421    injections: &[(&str, &str)],
422    imports: &[(&str, Option<&str>)],
423) -> Result<Vc<Box<dyn Source>>> {
424    let template_path = virtual_next_js_template_path(project_path.clone(), template_path).await?;
425
426    let content = file_content_rope(template_path.read()).await?;
427    let content = content.to_str()?;
428
429    let package_root = get_next_package(project_path).await?;
430
431    let content = expand_next_js_template_no_imports(
432        &content,
433        &template_path.path,
434        &package_root.path,
435        replacements.iter().copied(),
436        injections.iter().copied(),
437        imports.iter().copied(),
438    )?;
439
440    let file = File::from(content);
441    let source = VirtualSource::new(
442        template_path,
443        AssetContent::file(FileContent::Content(file).cell()),
444    );
445
446    Ok(Vc::upcast(source))
447}
448
449#[turbo_tasks::function]
450pub async fn file_content_rope(content: Vc<FileContent>) -> Result<Vc<Rope>> {
451    let content = &*content.await?;
452
453    let FileContent::Content(file) = content else {
454        bail!("Expected file content for file");
455    };
456
457    Ok(file.content().to_owned().cell())
458}
459
460async fn virtual_next_js_template_path(
461    project_path: FileSystemPath,
462    file: &str,
463) -> Result<FileSystemPath> {
464    debug_assert!(!file.contains('/'));
465    get_next_package(project_path)
466        .await?
467        .join(&format!("{NEXT_TEMPLATE_PATH}/{file}"))
468}
469
470pub async fn load_next_js_json_file<T: DeserializeOwned>(
471    project_path: FileSystemPath,
472    sub_path: RcStr,
473) -> Result<T> {
474    let file_path = get_next_package(project_path.clone())
475        .await?
476        .join(&sub_path)?;
477
478    let content = &*file_path.read().await?;
479
480    match content.parse_json_ref() {
481        FileJsonContent::Unparsable(e) => bail!("File is not valid JSON: {e}"),
482        FileJsonContent::NotFound => turbobail!("File not found: {file_path:?}",),
483        FileJsonContent::Content(value) => Ok(serde_json::from_value(value)?),
484    }
485}
486
487pub async fn load_next_js_jsonc_file<T: DeserializeOwned>(
488    project_path: FileSystemPath,
489    sub_path: RcStr,
490) -> Result<T> {
491    let file_path = get_next_package(project_path.clone())
492        .await?
493        .join(&sub_path)?;
494
495    let content = &*file_path.read().await?;
496
497    match content.parse_json_with_comments_ref() {
498        FileJsonContent::Unparsable(e) => turbobail!("File is not valid JSON: {e}"),
499        FileJsonContent::NotFound => turbobail!("File not found: {file_path}",),
500        FileJsonContent::Content(value) => Ok(serde_json::from_value(value)?),
501    }
502}
503
504pub fn styles_rule_condition() -> RuleCondition {
505    RuleCondition::any(vec![
506        RuleCondition::all(vec![
507            RuleCondition::ResourcePathEndsWith(".css".into()),
508            RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.css".into())),
509        ]),
510        RuleCondition::all(vec![
511            RuleCondition::ResourcePathEndsWith(".sass".into()),
512            RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.sass".into())),
513        ]),
514        RuleCondition::all(vec![
515            RuleCondition::ResourcePathEndsWith(".scss".into()),
516            RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.scss".into())),
517        ]),
518        RuleCondition::all(vec![
519            RuleCondition::ContentTypeStartsWith("text/css".into()),
520            RuleCondition::not(RuleCondition::ContentTypeStartsWith(
521                "text/css+module".into(),
522            )),
523        ]),
524        RuleCondition::all(vec![
525            RuleCondition::ContentTypeStartsWith("text/sass".into()),
526            RuleCondition::not(RuleCondition::ContentTypeStartsWith(
527                "text/sass+module".into(),
528            )),
529        ]),
530        RuleCondition::all(vec![
531            RuleCondition::ContentTypeStartsWith("text/scss".into()),
532            RuleCondition::not(RuleCondition::ContentTypeStartsWith(
533                "text/scss+module".into(),
534            )),
535        ]),
536    ])
537}
538pub fn module_styles_rule_condition() -> RuleCondition {
539    RuleCondition::any(vec![
540        RuleCondition::ResourcePathEndsWith(".module.css".into()),
541        RuleCondition::ResourcePathEndsWith(".module.scss".into()),
542        RuleCondition::ResourcePathEndsWith(".module.sass".into()),
543        RuleCondition::ContentTypeStartsWith("text/css+module".into()),
544        RuleCondition::ContentTypeStartsWith("text/sass+module".into()),
545        RuleCondition::ContentTypeStartsWith("text/scss+module".into()),
546    ])
547}
548
549/// Returns the list of global variables that should be forwarded from the main
550/// context to web workers. These are Next.js-specific globals that need to be
551/// available in worker contexts.
552pub fn worker_forwarded_globals() -> Vec<RcStr> {
553    vec![
554        rcstr!("NEXT_DEPLOYMENT_ID"),
555        rcstr!("NEXT_CLIENT_ASSET_SUFFIX"),
556    ]
557}
558
559/// The globs defined in the next.config.mjs are relative to the project root.
560/// The glob walker in turbopack is somewhat naive so we handle relative path directives first so
561/// traversal doesn't need to consider them and can just traverse 'down' the tree.
562/// The main alternative is to merge glob evaluation with directory traversal which is what the npm
563/// `glob` package does, but this would be a substantial rewrite.
564pub fn relativize_glob<'a>(
565    glob: &'a str,
566    relative_to: &FileSystemPath,
567) -> Result<(&'a str, FileSystemPath)> {
568    let mut relative_to = Cow::Borrowed(relative_to);
569    let mut processed_glob = glob;
570    loop {
571        if let Some(stripped) = processed_glob.strip_prefix("../") {
572            if relative_to.path.is_empty() {
573                bail!(
574                    "glob '{glob}' is invalid, it has a prefix that navigates out of the project \
575                     root"
576                );
577            }
578            relative_to = Cow::Owned(relative_to.parent());
579            processed_glob = stripped;
580        } else if let Some(stripped) = processed_glob.strip_prefix("./") {
581            processed_glob = stripped;
582        } else {
583            break;
584        }
585    }
586    Ok((processed_glob, relative_to.into_owned()))
587}
588
589#[cfg(test)]
590mod tests {
591    use turbo_tasks::ResolvedVc;
592    use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
593    use turbo_tasks_fs::{FileSystemPath, NullFileSystem};
594
595    use super::*;
596
597    fn create_test_fs_path(path: &str) -> FileSystemPath {
598        FileSystemPath {
599            fs: ResolvedVc::upcast(NullFileSystem {}.resolved_cell()),
600            path: path.into(),
601        }
602    }
603
604    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
605    async fn test_relativize_glob_normal_patterns() {
606        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
607            BackendOptions::default(),
608            noop_backing_storage(),
609        ));
610        tt.run_once(async {
611            // Test normal glob patterns without relative prefixes
612            let base_path = create_test_fs_path("project/src");
613
614            let (glob, path) = relativize_glob("*.js", &base_path).unwrap();
615            assert_eq!(glob, "*.js");
616            assert_eq!(path.path.as_str(), "project/src");
617
618            let (glob, path) = relativize_glob("components/**/*.tsx", &base_path).unwrap();
619            assert_eq!(glob, "components/**/*.tsx");
620            assert_eq!(path.path.as_str(), "project/src");
621
622            let (glob, path) = relativize_glob("lib/utils.ts", &base_path).unwrap();
623            assert_eq!(glob, "lib/utils.ts");
624            assert_eq!(path.path.as_str(), "project/src");
625            Ok(())
626        })
627        .await
628        .unwrap();
629    }
630
631    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
632    async fn test_relativize_glob_current_directory_prefix() {
633        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
634            BackendOptions::default(),
635            noop_backing_storage(),
636        ));
637        tt.run_once(async {
638            let base_path = create_test_fs_path("project/src");
639
640            // Single ./ prefix
641            let (glob, path) = relativize_glob("./components/*.tsx", &base_path).unwrap();
642            assert_eq!(glob, "components/*.tsx");
643            assert_eq!(path.path.as_str(), "project/src");
644
645            // Multiple ./ prefixes
646            let (glob, path) = relativize_glob("././utils.js", &base_path).unwrap();
647            assert_eq!(glob, "utils.js");
648            assert_eq!(path.path.as_str(), "project/src");
649
650            // ./ with complex glob
651            let (glob, path) = relativize_glob("./lib/**/*.{js,ts}", &base_path).unwrap();
652            assert_eq!(glob, "lib/**/*.{js,ts}");
653            assert_eq!(path.path.as_str(), "project/src");
654            Ok(())
655        })
656        .await
657        .unwrap();
658    }
659
660    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
661    async fn test_relativize_glob_parent_directory_navigation() {
662        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
663            BackendOptions::default(),
664            noop_backing_storage(),
665        ));
666        tt.run_once(async {
667            let base_path = create_test_fs_path("project/src/components");
668
669            // Single ../ prefix
670            let (glob, path) = relativize_glob("../utils/*.js", &base_path).unwrap();
671            assert_eq!(glob, "utils/*.js");
672            assert_eq!(path.path.as_str(), "project/src");
673
674            // Multiple ../ prefixes
675            let (glob, path) = relativize_glob("../../lib/*.ts", &base_path).unwrap();
676            assert_eq!(glob, "lib/*.ts");
677            assert_eq!(path.path.as_str(), "project");
678
679            // Complex navigation with glob
680            let (glob, path) = relativize_glob("../../../external/**/*.json", &base_path).unwrap();
681            assert_eq!(glob, "external/**/*.json");
682            assert_eq!(path.path.as_str(), "");
683            Ok(())
684        })
685        .await
686        .unwrap();
687    }
688
689    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
690    async fn test_relativize_glob_mixed_prefixes() {
691        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
692            BackendOptions::default(),
693            noop_backing_storage(),
694        ));
695        tt.run_once(async {
696            let base_path = create_test_fs_path("project/src/components");
697
698            // ../ followed by ./
699            let (glob, path) = relativize_glob(".././utils/*.js", &base_path).unwrap();
700            assert_eq!(glob, "utils/*.js");
701            assert_eq!(path.path.as_str(), "project/src");
702
703            // ./ followed by ../
704            let (glob, path) = relativize_glob("./../lib/*.ts", &base_path).unwrap();
705            assert_eq!(glob, "lib/*.ts");
706            assert_eq!(path.path.as_str(), "project/src");
707
708            // Multiple mixed prefixes
709            let (glob, path) = relativize_glob("././../.././external/*.json", &base_path).unwrap();
710            assert_eq!(glob, "external/*.json");
711            assert_eq!(path.path.as_str(), "project");
712            Ok(())
713        })
714        .await
715        .unwrap();
716    }
717
718    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
719    async fn test_relativize_glob_error_navigation_out_of_root() {
720        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
721            BackendOptions::default(),
722            noop_backing_storage(),
723        ));
724        tt.run_once(async {
725            // Test navigating out of project root with empty path
726            let empty_path = create_test_fs_path("");
727            let result = relativize_glob("../outside.js", &empty_path);
728            assert!(result.is_err());
729            assert!(
730                result
731                    .unwrap_err()
732                    .to_string()
733                    .contains("navigates out of the project root")
734            );
735
736            // Test navigating too far up from a shallow path
737            let shallow_path = create_test_fs_path("project");
738            let result = relativize_glob("../../outside.js", &shallow_path);
739            assert!(result.is_err());
740            assert!(
741                result
742                    .unwrap_err()
743                    .to_string()
744                    .contains("navigates out of the project root")
745            );
746
747            // Test multiple ../ that would go out of root
748            let base_path = create_test_fs_path("a/b");
749            let result = relativize_glob("../../../outside.js", &base_path);
750            assert!(result.is_err());
751            assert!(
752                result
753                    .unwrap_err()
754                    .to_string()
755                    .contains("navigates out of the project root")
756            );
757            Ok(())
758        })
759        .await
760        .unwrap();
761    }
762}