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