1use std::future::Future;
2
3use anyhow::{Context, Result, bail};
4use serde::{Deserialize, Serialize, de::DeserializeOwned};
5use swc_core::{
6 common::GLOBALS,
7 ecma::ast::{Expr, Lit, Program},
8};
9use turbo_rcstr::RcStr;
10use turbo_tasks::{
11 FxIndexMap, FxIndexSet, NonLocalValue, ResolvedVc, TaskInput, ValueDefault, ValueToString, Vc,
12 trace::TraceRawVcs, util::WrapFuture,
13};
14use turbo_tasks_fs::{
15 self, File, FileContent, FileSystem, FileSystemPath, json::parse_json_rope_with_source_context,
16 rope::Rope, util::join_path,
17};
18use turbopack_core::{
19 asset::AssetContent,
20 condition::ContextCondition,
21 ident::AssetIdent,
22 issue::{Issue, IssueExt, IssueSeverity, IssueStage, OptionStyledString, StyledString},
23 module::Module,
24 source::Source,
25 virtual_source::VirtualSource,
26};
27use turbopack_ecmascript::{
28 EcmascriptParsable,
29 analyzer::{ConstantValue, JsValue, ObjectPart},
30 parse::ParseResult,
31 utils::StringifyJs,
32};
33
34use crate::{
35 embed_js::next_js_fs,
36 next_config::{NextConfig, RouteHas},
37 next_import_map::get_next_package,
38 next_manifests::MiddlewareMatcher,
39};
40
41const NEXT_TEMPLATE_PATH: &str = "dist/esm/build/templates";
42
43#[derive(
44 Debug, Clone, Copy, PartialEq, Eq, Hash, TaskInput, Serialize, Deserialize, TraceRawVcs,
45)]
46pub enum PathType {
47 PagesPage,
48 PagesApi,
49 Data,
50}
51
52#[turbo_tasks::function]
54pub async fn pathname_for_path(
55 server_root: Vc<FileSystemPath>,
56 server_path: Vc<FileSystemPath>,
57 path_ty: PathType,
58) -> Result<Vc<RcStr>> {
59 let server_path_value = &*server_path.await?;
60 let path = if let Some(path) = server_root.await?.get_path_to(server_path_value) {
61 path
62 } else {
63 bail!(
64 "server_path ({}) is not in server_root ({})",
65 server_path.to_string().await?,
66 server_root.to_string().await?
67 )
68 };
69 let path = match (path_ty, path) {
70 (PathType::Data, "") => "/index".into(),
72 (_, path) => format!("/{path}").into(),
75 };
76
77 Ok(Vc::cell(path))
78}
79
80pub fn get_asset_prefix_from_pathname(pathname: &str) -> String {
84 if pathname == "/" {
85 "/index".to_string()
86 } else if pathname == "/index" || pathname.starts_with("/index/") {
87 format!("/index{pathname}")
88 } else {
89 pathname.to_string()
90 }
91}
92
93pub fn get_asset_path_from_pathname(pathname: &str, ext: &str) -> String {
95 format!("{}{}", get_asset_prefix_from_pathname(pathname), ext)
96}
97
98#[turbo_tasks::function]
99pub async fn get_transpiled_packages(
100 next_config: Vc<NextConfig>,
101 project_path: ResolvedVc<FileSystemPath>,
102) -> Result<Vc<Vec<RcStr>>> {
103 let mut transpile_packages: Vec<RcStr> = next_config.transpile_packages().owned().await?;
104
105 let default_transpiled_packages: Vec<RcStr> = load_next_js_templateon(
106 project_path,
107 "dist/lib/default-transpiled-packages.json".into(),
108 )
109 .await?;
110
111 transpile_packages.extend(default_transpiled_packages.iter().cloned());
112
113 Ok(Vc::cell(transpile_packages))
114}
115
116pub async fn foreign_code_context_condition(
117 next_config: Vc<NextConfig>,
118 project_path: ResolvedVc<FileSystemPath>,
119) -> Result<ContextCondition> {
120 let transpiled_packages = get_transpiled_packages(next_config, *project_path).await?;
121
122 let not_next_template_dir = ContextCondition::not(ContextCondition::InPath(
127 get_next_package(*project_path)
128 .join(NEXT_TEMPLATE_PATH.into())
129 .to_resolved()
130 .await?,
131 ));
132
133 let result = ContextCondition::all(vec![
134 ContextCondition::InDirectory("node_modules".to_string()),
135 not_next_template_dir,
136 ContextCondition::not(ContextCondition::any(
137 transpiled_packages
138 .iter()
139 .map(|package| ContextCondition::InDirectory(format!("node_modules/{package}")))
140 .collect(),
141 )),
142 ]);
143 Ok(result)
144}
145
146pub async fn internal_assets_conditions() -> Result<ContextCondition> {
153 Ok(ContextCondition::any(vec![
154 ContextCondition::InPath(next_js_fs().root().to_resolved().await?),
155 ContextCondition::InPath(
156 turbopack_ecmascript_runtime::embed_fs()
157 .root()
158 .to_resolved()
159 .await?,
160 ),
161 ContextCondition::InPath(
162 turbopack_node::embed_js::embed_fs()
163 .root()
164 .to_resolved()
165 .await?,
166 ),
167 ]))
168}
169
170#[derive(
171 Default,
172 PartialEq,
173 Eq,
174 Clone,
175 Copy,
176 Debug,
177 TraceRawVcs,
178 Serialize,
179 Deserialize,
180 Hash,
181 PartialOrd,
182 Ord,
183 TaskInput,
184 NonLocalValue,
185)]
186#[serde(rename_all = "lowercase")]
187pub enum NextRuntime {
188 #[default]
189 NodeJs,
190 #[serde(alias = "experimental-edge")]
191 Edge,
192}
193
194impl NextRuntime {
195 pub fn conditions(&self) -> &'static [&'static str] {
196 match self {
197 NextRuntime::NodeJs => &["node"],
198 NextRuntime::Edge => &["edge-light"],
199 }
200 }
201}
202
203#[turbo_tasks::value]
204#[derive(Debug, Clone)]
205pub enum MiddlewareMatcherKind {
206 Str(String),
207 Matcher(MiddlewareMatcher),
208}
209
210#[turbo_tasks::value]
211#[derive(Default, Clone)]
212pub struct NextSourceConfig {
213 pub runtime: NextRuntime,
214
215 pub matcher: Option<Vec<MiddlewareMatcherKind>>,
217
218 pub regions: Option<Vec<RcStr>>,
219}
220
221#[turbo_tasks::value_impl]
222impl ValueDefault for NextSourceConfig {
223 #[turbo_tasks::function]
224 pub fn value_default() -> Vc<Self> {
225 NextSourceConfig::default().cell()
226 }
227}
228
229#[turbo_tasks::value(shared)]
231pub struct NextSourceConfigParsingIssue {
232 ident: ResolvedVc<AssetIdent>,
233 detail: ResolvedVc<StyledString>,
234}
235
236#[turbo_tasks::value_impl]
237impl NextSourceConfigParsingIssue {
238 #[turbo_tasks::function]
239 pub fn new(ident: ResolvedVc<AssetIdent>, detail: ResolvedVc<StyledString>) -> Vc<Self> {
240 Self { ident, detail }.cell()
241 }
242}
243
244#[turbo_tasks::value_impl]
245impl Issue for NextSourceConfigParsingIssue {
246 #[turbo_tasks::function]
247 fn severity(&self) -> Vc<IssueSeverity> {
248 IssueSeverity::Warning.into()
249 }
250
251 #[turbo_tasks::function]
252 fn title(&self) -> Vc<StyledString> {
253 StyledString::Text("Unable to parse config export in source file".into()).cell()
254 }
255
256 #[turbo_tasks::function]
257 fn stage(&self) -> Vc<IssueStage> {
258 IssueStage::Parse.into()
259 }
260
261 #[turbo_tasks::function]
262 fn file_path(&self) -> Vc<FileSystemPath> {
263 self.ident.path()
264 }
265
266 #[turbo_tasks::function]
267 fn description(&self) -> Vc<OptionStyledString> {
268 Vc::cell(Some(
269 StyledString::Text(
270 "The exported configuration object in a source file need to have a very specific \
271 format from which some properties can be statically parsed at compiled-time."
272 .into(),
273 )
274 .resolved_cell(),
275 ))
276 }
277
278 #[turbo_tasks::function]
279 fn detail(&self) -> Vc<OptionStyledString> {
280 Vc::cell(Some(self.detail))
281 }
282}
283
284async fn emit_invalid_config_warning(
285 ident: Vc<AssetIdent>,
286 detail: &str,
287 value: &JsValue,
288) -> Result<()> {
289 let (explainer, hints) = value.explain(2, 0);
290 NextSourceConfigParsingIssue::new(
291 ident,
292 StyledString::Text(format!("{detail} Got {explainer}.{hints}").into()).cell(),
293 )
294 .to_resolved()
295 .await?
296 .emit();
297 Ok(())
298}
299
300async fn parse_route_matcher_from_js_value(
301 ident: Vc<AssetIdent>,
302 value: &JsValue,
303) -> Result<Option<Vec<MiddlewareMatcherKind>>> {
304 let parse_matcher_kind_matcher = |value: &JsValue| {
305 let mut route_has = vec![];
306 if let JsValue::Array { items, .. } = value {
307 for item in items {
308 if let JsValue::Object { parts, .. } = item {
309 let mut route_type = None;
310 let mut route_key = None;
311 let mut route_value = None;
312
313 for matcher_part in parts {
314 if let ObjectPart::KeyValue(part_key, part_value) = matcher_part {
315 match part_key.as_str() {
316 Some("type") => {
317 route_type = part_value.as_str().map(|v| v.to_string())
318 }
319 Some("key") => {
320 route_key = part_value.as_str().map(|v| v.to_string())
321 }
322 Some("value") => {
323 route_value = part_value.as_str().map(|v| v.to_string())
324 }
325 _ => {}
326 }
327 }
328 }
329 let r = match route_type.as_deref() {
330 Some("header") => route_key.map(|route_key| RouteHas::Header {
331 key: route_key.into(),
332 value: route_value.map(From::from),
333 }),
334 Some("cookie") => route_key.map(|route_key| RouteHas::Cookie {
335 key: route_key.into(),
336 value: route_value.map(From::from),
337 }),
338 Some("query") => route_key.map(|route_key| RouteHas::Query {
339 key: route_key.into(),
340 value: route_value.map(From::from),
341 }),
342 Some("host") => route_value.map(|route_value| RouteHas::Host {
343 value: route_value.into(),
344 }),
345 _ => None,
346 };
347
348 if let Some(r) = r {
349 route_has.push(r);
350 }
351 }
352 }
353 }
354
355 route_has
356 };
357
358 let mut matchers = vec![];
359
360 match value {
361 JsValue::Constant(matcher) => {
362 if let Some(matcher) = matcher.as_str() {
363 matchers.push(MiddlewareMatcherKind::Str(matcher.to_string()));
364 } else {
365 emit_invalid_config_warning(
366 ident,
367 "The matcher property must be a string or array of strings",
368 value,
369 )
370 .await?;
371 }
372 }
373 JsValue::Array { items, .. } => {
374 for item in items {
375 if let Some(matcher) = item.as_str() {
376 matchers.push(MiddlewareMatcherKind::Str(matcher.to_string()));
377 } else if let JsValue::Object { parts, .. } = item {
378 let mut matcher = MiddlewareMatcher::default();
379 for matcher_part in parts {
380 if let ObjectPart::KeyValue(key, value) = matcher_part {
381 match key.as_str() {
382 Some("source") => {
383 if let Some(value) = value.as_str() {
384 matcher.original_source = value.into();
385 }
386 }
387 Some("missing") => {
388 matcher.missing = Some(parse_matcher_kind_matcher(value))
389 }
390 Some("has") => {
391 matcher.has = Some(parse_matcher_kind_matcher(value))
392 }
393 _ => {
394 }
396 }
397 }
398 }
399
400 matchers.push(MiddlewareMatcherKind::Matcher(matcher));
401 } else {
402 emit_invalid_config_warning(
403 ident,
404 "The matcher property must be a string or array of strings",
405 value,
406 )
407 .await?;
408 }
409 }
410 }
411 _ => {
412 emit_invalid_config_warning(
413 ident,
414 "The matcher property must be a string or array of strings",
415 value,
416 )
417 .await?
418 }
419 }
420
421 Ok(if matchers.is_empty() {
422 None
423 } else {
424 Some(matchers)
425 })
426}
427
428#[turbo_tasks::function]
429pub async fn parse_config_from_source(
430 module: ResolvedVc<Box<dyn Module>>,
431 default_runtime: NextRuntime,
432) -> Result<Vc<NextSourceConfig>> {
433 if let Some(ecmascript_asset) = ResolvedVc::try_sidecast::<Box<dyn EcmascriptParsable>>(module)
434 {
435 if let ParseResult::Ok {
436 program: Program::Module(module_ast),
437 globals,
438 eval_context,
439 ..
440 } = &*ecmascript_asset.parse_original().await?
441 {
442 for item in &module_ast.body {
443 if let Some(decl) = item
444 .as_module_decl()
445 .and_then(|mod_decl| mod_decl.as_export_decl())
446 .and_then(|export_decl| export_decl.decl.as_var())
447 {
448 for decl in &decl.decls {
449 let decl_ident = decl.name.as_ident();
450
451 if decl_ident
454 .map(|ident| &*ident.sym == "config")
455 .unwrap_or_default()
456 {
457 if let Some(init) = decl.init.as_ref() {
458 return WrapFuture::new(
459 async {
460 let value = eval_context.eval(init);
461 Ok(parse_config_from_js_value(
462 *module,
463 &value,
464 default_runtime,
465 )
466 .await?
467 .cell())
468 },
469 |f, ctx| GLOBALS.set(globals, || f.poll(ctx)),
470 )
471 .await;
472 } else {
473 NextSourceConfigParsingIssue::new(
474 module.ident(),
475 StyledString::Text(
476 "The exported config object must contain an variable \
477 initializer."
478 .into(),
479 )
480 .cell(),
481 )
482 .to_resolved()
483 .await?
484 .emit();
485 }
486 }
487 else if decl_ident
490 .map(|ident| &*ident.sym == "runtime")
491 .unwrap_or_default()
492 {
493 let runtime_value_issue = NextSourceConfigParsingIssue::new(
494 module.ident(),
495 StyledString::Text(
496 "The runtime property must be either \"nodejs\" or \"edge\"."
497 .into(),
498 )
499 .cell(),
500 )
501 .to_resolved()
502 .await?;
503 if let Some(init) = decl.init.as_ref() {
504 if let Expr::Lit(Lit::Str(str_value)) = &**init {
507 let mut config = NextSourceConfig::default();
508
509 let runtime = str_value.value.to_string();
510 match runtime.as_str() {
511 "edge" | "experimental-edge" => {
512 config.runtime = NextRuntime::Edge;
513 }
514 "nodejs" => {
515 config.runtime = NextRuntime::NodeJs;
516 }
517 _ => {
518 runtime_value_issue.emit();
519 }
520 }
521
522 return Ok(config.cell());
523 } else {
524 runtime_value_issue.emit();
525 }
526 } else {
527 NextSourceConfigParsingIssue::new(
528 module.ident(),
529 StyledString::Text(
530 "The exported segment runtime option must contain an \
531 variable initializer."
532 .into(),
533 )
534 .cell(),
535 )
536 .to_resolved()
537 .await?
538 .emit();
539 }
540 }
541 }
542 }
543 }
544 }
545 }
546 let config = NextSourceConfig {
547 runtime: default_runtime,
548 ..Default::default()
549 };
550
551 Ok(config.cell())
552}
553
554async fn parse_config_from_js_value(
555 module: Vc<Box<dyn Module>>,
556 value: &JsValue,
557 default_runtime: NextRuntime,
558) -> Result<NextSourceConfig> {
559 let mut config = NextSourceConfig {
560 runtime: default_runtime,
561 ..Default::default()
562 };
563
564 if let JsValue::Object { parts, .. } = value {
565 for part in parts {
566 match part {
567 ObjectPart::Spread(_) => {
568 emit_invalid_config_warning(
569 module.ident(),
570 "Spread properties are not supported in the config export.",
571 value,
572 )
573 .await?
574 }
575 ObjectPart::KeyValue(key, value) => {
576 if let Some(key) = key.as_str() {
577 match key {
578 "runtime" => {
579 if let JsValue::Constant(runtime) = value {
580 if let Some(runtime) = runtime.as_str() {
581 match runtime {
582 "edge" | "experimental-edge" => {
583 config.runtime = NextRuntime::Edge;
584 }
585 "nodejs" => {
586 config.runtime = NextRuntime::NodeJs;
587 }
588 _ => {
589 emit_invalid_config_warning(
590 module.ident(),
591 "The runtime property must be either \
592 \"nodejs\" or \"edge\".",
593 value,
594 )
595 .await?;
596 }
597 }
598 }
599 } else {
600 emit_invalid_config_warning(
601 module.ident(),
602 "The runtime property must be a constant string.",
603 value,
604 )
605 .await?;
606 }
607 }
608 "matcher" => {
609 config.matcher =
610 parse_route_matcher_from_js_value(module.ident(), value)
611 .await?;
612 }
613 "regions" => {
614 config.regions = match value {
615 JsValue::Constant(ConstantValue::Str(str)) => {
617 Some(vec![str.to_string().into()])
618 }
619 JsValue::Array { items, .. } => {
623 let mut regions: Vec<RcStr> = Vec::new();
624 for item in items {
625 if let JsValue::Constant(ConstantValue::Str(str)) = item
626 {
627 regions.push(str.to_string().into());
628 } else {
629 emit_invalid_config_warning(
630 module.ident(),
631 "Values of the `config.regions` array need to \
632 static strings",
633 item,
634 )
635 .await?;
636 }
637 }
638 Some(regions)
639 }
640 _ => {
641 emit_invalid_config_warning(
642 module.ident(),
643 "`config.regions` needs to be a static string or \
644 array of static strings",
645 value,
646 )
647 .await?;
648 None
649 }
650 };
651 }
652 _ => {}
653 }
654 } else {
655 emit_invalid_config_warning(
656 module.ident(),
657 "The exported config object must not contain non-constant strings.",
658 key,
659 )
660 .await?;
661 }
662 }
663 }
664 }
665 } else {
666 emit_invalid_config_warning(
667 module.ident(),
668 "The exported config object must be a valid object literal.",
669 value,
670 )
671 .await?;
672 }
673
674 Ok(config)
675}
676
677pub async fn load_next_js_template(
680 path: &str,
681 project_path: Vc<FileSystemPath>,
682 replacements: FxIndexMap<&'static str, RcStr>,
683 injections: FxIndexMap<&'static str, RcStr>,
684 imports: FxIndexMap<&'static str, Option<RcStr>>,
685) -> Result<Vc<Box<dyn Source>>> {
686 let path = virtual_next_js_template_path(project_path, path.to_string());
687
688 let content = &*file_content_rope(path.read()).await?;
689 let content = content.to_str()?.into_owned();
690
691 let parent_path = path.parent();
692 let parent_path_value = &*parent_path.await?;
693
694 let package_root = get_next_package(project_path).parent();
695 let package_root_value = &*package_root.await?;
696
697 fn replace_all<E>(
699 re: ®ex::Regex,
700 haystack: &str,
701 mut replacement: impl FnMut(®ex::Captures) -> Result<String, E>,
702 ) -> Result<String, E> {
703 let mut new = String::with_capacity(haystack.len());
704 let mut last_match = 0;
705 for caps in re.captures_iter(haystack) {
706 let m = caps.get(0).unwrap();
707 new.push_str(&haystack[last_match..m.start()]);
708 new.push_str(&replacement(&caps)?);
709 last_match = m.end();
710 }
711 new.push_str(&haystack[last_match..]);
712 Ok(new)
713 }
714
715 let regex = lazy_regex::regex!("(?:from '(\\..*)'|import '(\\..*)')");
718
719 let mut count = 0;
720 let mut content = replace_all(regex, &content, |caps| {
721 let from_request = caps.get(1).map_or("", |c| c.as_str());
722 let import_request = caps.get(2).map_or("", |c| c.as_str());
723
724 count += 1;
725 let is_from_request = !from_request.is_empty();
726
727 let imported = FileSystemPath {
728 fs: package_root_value.fs,
729 path: join_path(
730 &parent_path_value.path,
731 if is_from_request {
732 from_request
733 } else {
734 import_request
735 },
736 )
737 .context("path should not leave the fs")?
738 .into(),
739 };
740
741 let relative = package_root_value
742 .get_relative_path_to(&imported)
743 .context("path has to be relative to package root")?;
744
745 if !relative.starts_with("./next/") {
746 bail!(
747 "Invariant: Expected relative import to start with \"./next/\", found \"{}\"",
748 relative
749 )
750 }
751
752 let relative = relative
753 .strip_prefix("./")
754 .context("should be able to strip the prefix")?;
755
756 Ok(if is_from_request {
757 format!("from {}", StringifyJs(relative))
758 } else {
759 format!("import {}", StringifyJs(relative))
760 })
761 })
762 .context("replacing imports failed")?;
763
764 if count == 0 {
769 bail!("Invariant: Expected to replace at least one import")
770 }
771
772 let mut replaced = FxIndexSet::default();
775 for (key, replacement) in &replacements {
776 let full = format!("'{key}'");
777
778 if content.contains(&full) {
779 replaced.insert(*key);
780 content = content.replace(&full, &StringifyJs(&replacement).to_string());
781 }
782 }
783
784 let regex = lazy_regex::regex!("/VAR_[A-Z_]+");
786 let matches = regex
787 .find_iter(&content)
788 .map(|m| m.as_str().to_string())
789 .collect::<Vec<_>>();
790
791 if !matches.is_empty() {
792 bail!(
793 "Invariant: Expected to replace all template variables, found {}",
794 matches.join(", "),
795 )
796 }
797
798 if replaced.len() != replacements.len() {
800 let difference = replacements
804 .keys()
805 .filter(|k| !replaced.contains(*k))
806 .cloned()
807 .collect::<Vec<_>>();
808
809 bail!(
810 "Invariant: Expected to replace all template variables, missing {} in template",
811 difference.join(", "),
812 )
813 }
814
815 let mut injected = FxIndexSet::default();
817 for (key, injection) in &injections {
818 let full = format!("// INJECT:{key}");
819
820 if content.contains(&full) {
821 injected.insert(*key);
823 content = content.replace(&full, &format!("const {key} = {injection}"));
824 }
825 }
826
827 let regex = lazy_regex::regex!("// INJECT:[A-Za-z0-9_]+");
829 let matches = regex
830 .find_iter(&content)
831 .map(|m| m.as_str().to_string())
832 .collect::<Vec<_>>();
833
834 if !matches.is_empty() {
835 bail!(
836 "Invariant: Expected to inject all injections, found {}",
837 matches.join(", "),
838 )
839 }
840
841 if injected.len() != injections.len() {
843 let difference = injections
847 .keys()
848 .filter(|k| !injected.contains(*k))
849 .cloned()
850 .collect::<Vec<_>>();
851
852 bail!(
853 "Invariant: Expected to inject all injections, missing {} in template",
854 difference.join(", "),
855 )
856 }
857
858 let mut imports_added = FxIndexSet::default();
860 for (key, import_path) in &imports {
861 let mut full = format!("// OPTIONAL_IMPORT:{key}");
862 let namespace = if !content.contains(&full) {
863 full = format!("// OPTIONAL_IMPORT:* as {key}");
864 if content.contains(&full) {
865 true
866 } else {
867 continue;
868 }
869 } else {
870 false
871 };
872
873 imports_added.insert(*key);
875
876 if let Some(path) = import_path {
877 content = content.replace(
878 &full,
879 &format!(
880 "import {}{} from {}",
881 if namespace { "* as " } else { "" },
882 key,
883 &StringifyJs(&path).to_string()
884 ),
885 );
886 } else {
887 content = content.replace(&full, &format!("const {key} = null"));
888 }
889 }
890
891 let regex = lazy_regex::regex!("// OPTIONAL_IMPORT:(\\* as )?[A-Za-z0-9_]+");
893 let matches = regex
894 .find_iter(&content)
895 .map(|m| m.as_str().to_string())
896 .collect::<Vec<_>>();
897
898 if !matches.is_empty() {
899 bail!(
900 "Invariant: Expected to inject all imports, found {}",
901 matches.join(", "),
902 )
903 }
904
905 if imports_added.len() != imports.len() {
907 let difference = imports
911 .keys()
912 .filter(|k| !imports_added.contains(*k))
913 .cloned()
914 .collect::<Vec<_>>();
915
916 bail!(
917 "Invariant: Expected to inject all imports, missing {} in template",
918 difference.join(", "),
919 )
920 }
921
922 if !content.ends_with('\n') {
924 content.push('\n');
925 }
926
927 let file = File::from(content);
928
929 let source = VirtualSource::new(path, AssetContent::file(file.into()));
930
931 Ok(Vc::upcast(source))
932}
933
934#[turbo_tasks::function]
935pub async fn file_content_rope(content: Vc<FileContent>) -> Result<Vc<Rope>> {
936 let content = &*content.await?;
937
938 let FileContent::Content(file) = content else {
939 bail!("Expected file content for file");
940 };
941
942 Ok(file.content().to_owned().cell())
943}
944
945pub fn virtual_next_js_template_path(
946 project_path: Vc<FileSystemPath>,
947 file: String,
948) -> Vc<FileSystemPath> {
949 debug_assert!(!file.contains('/'));
950 get_next_package(project_path).join(format!("{NEXT_TEMPLATE_PATH}/{file}").into())
951}
952
953pub async fn load_next_js_templateon<T: DeserializeOwned>(
954 project_path: ResolvedVc<FileSystemPath>,
955 path: RcStr,
956) -> Result<T> {
957 let file_path = get_next_package(*project_path).join(path.clone());
958
959 let content = &*file_path.read().await?;
960
961 let FileContent::Content(file) = content else {
962 bail!("Expected file content at {}", file_path.to_string().await?);
963 };
964
965 let result: T = parse_json_rope_with_source_context(file.content())?;
966
967 Ok(result)
968}