next_core/
util.rs

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/// As opposed to [`EnvMap`], this map allows for `None` values, which means that the variables
47/// should be replace with undefined.
48#[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/// Converts a filename within the server root into a next pathname.
90#[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        // "/" is special-cased to "/index" for data routes.
108        (PathType::Data, "") => "/index".into(),
109        // `get_path_to` always strips the leading `/` from the path, so we need to add
110        // it back here.
111        (_, path) => format!("/{path}").into(),
112    };
113
114    Ok(Vc::cell(path))
115}
116
117// Adapted from https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/get-asset-path-from-route.ts
118// TODO(alexkirsz) There's no need to create an intermediate string here (and
119// below), we should instead return an `impl Display`.
120pub 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
130// Adapted from https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/get-asset-path-from-route.ts
131pub 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    // The next template files are allowed to import the user's code via import
160    // mapping, and imports must use the project-level [ResolveOptions] instead
161    // of the `node_modules` specific resolve options (the template files are
162    // technically node module files).
163    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
182/// Determines if the module is an internal asset (i.e overlay, fallback) coming from the embedded
183/// FS, don't apply user defined transforms.
184//
185// TODO: Turbopack specific embed fs paths should be handled by internals of Turbopack itself and
186// user config should not try to leak this. However, currently we apply few transform options
187// subject to Next.js's configuration even if it's embedded assets.
188pub 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    /// Middleware router matchers
252    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/// An issue that occurred while parsing the page config.
266#[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                                    //noop
439                                }
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                    // Check if there is exported config object `export const config = {...}`
496                    // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
497                    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                    // Or, check if there is segment runtime option
538                    // https://nextjs.org/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes#segment-runtime-Option
539                    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                            // skipping eval and directly read the expr's value, as we know it
557                            // should be a const string
558                            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                                    // Single value is turned into a single-element Vec.
669                                    JsValue::Constant(ConstantValue::Str(str)) => {
670                                        Some(vec![str.to_string().into()])
671                                    }
672                                    // Array of strings is turned into a Vec. If one of the values
673                                    // in not a String it will
674                                    // error.
675                                    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
730/// Loads a next.js template, replaces `replacements` and `injections` and makes
731/// sure there are none left over.
732pub 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    /// See [regex::Regex::replace_all].
751    fn replace_all<E>(
752        re: &regex::Regex,
753        haystack: &str,
754        mut replacement: impl FnMut(&regex::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    // Update the relative imports to be absolute. This will update any relative
769    // imports to be relative to the root of the `next` package.
770    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    // Verify that at least one import was replaced. It's the case today where
818    // every template file has at least one import to update, so this ensures that
819    // we don't accidentally remove the import replacement code or use the wrong
820    // template file.
821    if count == 0 {
822        bail!("Invariant: Expected to replace at least one import")
823    }
824
825    // Replace all the template variables with the actual values. If a template
826    // variable is missing, throw an error.
827    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    // Check to see if there's any remaining template variables.
838    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    // Check to see if any template variable was provided but not used.
852    if replaced.len() != replacements.len() {
853        // Find the difference between the provided replacements and the replaced
854        // template variables. This will let us notify the user of any template
855        // variables that were not used but were provided.
856        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    // Replace the injections.
869    let mut injected = FxIndexSet::default();
870    for (key, injection) in &injections {
871        let full = format!("// INJECT:{key}");
872
873        if content.contains(&full) {
874            // Track all the injections to ensure that we're not missing any.
875            injected.insert(*key);
876            content = content.replace(&full, &format!("const {key} = {injection}"));
877        }
878    }
879
880    // Check to see if there's any remaining injections.
881    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    // Check to see if any injection was provided but not used.
895    if injected.len() != injections.len() {
896        // Find the difference between the provided replacements and the replaced
897        // template variables. This will let us notify the user of any template
898        // variables that were not used but were provided.
899        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    // Replace the optional imports.
912    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        // Track all the imports to ensure that we're not missing any.
927        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    // Check to see if there's any remaining imports.
945    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    // Check to see if any import was provided but not used.
959    if imports_added.len() != imports.len() {
960        // Find the difference between the provided imports and the injected
961        // imports. This will let us notify the user of any imports that were
962        // not used but were provided.
963        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    // Ensure that the last line is a newline.
976    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}