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,
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/// Converts a filename within the server root into a next pathname.
53#[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        // "/" is special-cased to "/index" for data routes.
71        (PathType::Data, "") => "/index".into(),
72        // `get_path_to` always strips the leading `/` from the path, so we need to add
73        // it back here.
74        (_, path) => format!("/{path}").into(),
75    };
76
77    Ok(Vc::cell(path))
78}
79
80// Adapted from https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/get-asset-path-from-route.ts
81// TODO(alexkirsz) There's no need to create an intermediate string here (and
82// below), we should instead return an `impl Display`.
83pub 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
93// Adapted from https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/get-asset-path-from-route.ts
94pub 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    // The next template files are allowed to import the user's code via import
123    // mapping, and imports must use the project-level [ResolveOptions] instead
124    // of the `node_modules` specific resolve options (the template files are
125    // technically node module files).
126    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
146/// Determines if the module is an internal asset (i.e overlay, fallback) coming from the embedded
147/// FS, don't apply user defined transforms.
148//
149// TODO: Turbopack specific embed fs paths should be handled by internals of Turbopack itself and
150// user config should not try to leak this. However, currently we apply few transform options
151// subject to Next.js's configuration even if it's embedded assets.
152pub 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    /// Middleware router matchers
216    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/// An issue that occurred while parsing the page config.
230#[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                                    //noop
395                                }
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                        // Check if there is exported config object `export const config = {...}`
452                        // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
453                        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                        // Or, check if there is segment runtime option
488                        // https://nextjs.org/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes#segment-runtime-Option
489                        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                                // skipping eval and directly read the expr's value, as we know it
505                                // should be a const string
506                                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                                    // Single value is turned into a single-element Vec.
616                                    JsValue::Constant(ConstantValue::Str(str)) => {
617                                        Some(vec![str.to_string().into()])
618                                    }
619                                    // Array of strings is turned into a Vec. If one of the values
620                                    // in not a String it will
621                                    // error.
622                                    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
677/// Loads a next.js template, replaces `replacements` and `injections` and makes
678/// sure there are none left over.
679pub 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    /// See [regex::Regex::replace_all].
698    fn replace_all<E>(
699        re: &regex::Regex,
700        haystack: &str,
701        mut replacement: impl FnMut(&regex::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    // Update the relative imports to be absolute. This will update any relative
716    // imports to be relative to the root of the `next` package.
717    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    // Verify that at least one import was replaced. It's the case today where
765    // every template file has at least one import to update, so this ensures that
766    // we don't accidentally remove the import replacement code or use the wrong
767    // template file.
768    if count == 0 {
769        bail!("Invariant: Expected to replace at least one import")
770    }
771
772    // Replace all the template variables with the actual values. If a template
773    // variable is missing, throw an error.
774    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    // Check to see if there's any remaining template variables.
785    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    // Check to see if any template variable was provided but not used.
799    if replaced.len() != replacements.len() {
800        // Find the difference between the provided replacements and the replaced
801        // template variables. This will let us notify the user of any template
802        // variables that were not used but were provided.
803        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    // Replace the injections.
816    let mut injected = FxIndexSet::default();
817    for (key, injection) in &injections {
818        let full = format!("// INJECT:{key}");
819
820        if content.contains(&full) {
821            // Track all the injections to ensure that we're not missing any.
822            injected.insert(*key);
823            content = content.replace(&full, &format!("const {key} = {injection}"));
824        }
825    }
826
827    // Check to see if there's any remaining injections.
828    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    // Check to see if any injection was provided but not used.
842    if injected.len() != injections.len() {
843        // Find the difference between the provided replacements and the replaced
844        // template variables. This will let us notify the user of any template
845        // variables that were not used but were provided.
846        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    // Replace the optional imports.
859    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        // Track all the imports to ensure that we're not missing any.
874        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    // Check to see if there's any remaining imports.
892    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    // Check to see if any import was provided but not used.
906    if imports_added.len() != imports.len() {
907        // Find the difference between the provided imports and the injected
908        // imports. This will let us notify the user of any imports that were
909        // not used but were provided.
910        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    // Ensure that the last line is a newline.
923    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}