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#[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
65pub fn free_var_references_with_vercel_system_env_warnings(
67 defines: CompileTimeDefines,
68 severity: IssueSeverity,
69) -> FreeVarReferences {
70 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 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#[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 (PathType::Data, "") => rcstr!("/index"),
230 (_, path) => format!("/{path}").into(),
233 };
234
235 Ok(Vc::cell(path))
236}
237
238pub 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
251pub 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 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
303pub 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 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 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
387pub 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
421pub 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
556pub fn worker_forwarded_globals() -> Vec<RcStr> {
560 vec![
561 rcstr!("NEXT_DEPLOYMENT_ID"),
562 rcstr!("NEXT_CLIENT_ASSET_SUFFIX"),
563 ]
564}
565
566pub 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 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 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 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 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 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 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 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 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 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 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 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 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 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}