next_core/
util.rs

1use std::{fmt::Display, future::Future, str::FromStr};
2
3use anyhow::{Result, bail};
4use next_taskless::expand_next_js_template;
5use serde::{Deserialize, Serialize, de::DeserializeOwned};
6use swc_core::{
7    common::{GLOBALS, Spanned, source_map::SmallPos},
8    ecma::ast::{Expr, Lit, Program},
9};
10use turbo_rcstr::{RcStr, rcstr};
11use turbo_tasks::{
12    FxIndexMap, NonLocalValue, ResolvedVc, TaskInput, ValueDefault, Vc, trace::TraceRawVcs,
13    util::WrapFuture,
14};
15use turbo_tasks_fs::{
16    self, File, FileContent, FileSystem, FileSystemPath, json::parse_json_rope_with_source_context,
17    rope::Rope,
18};
19use turbopack::module_options::RuleCondition;
20use turbopack_core::{
21    asset::AssetContent,
22    compile_time_info::{CompileTimeDefineValue, CompileTimeDefines, DefinableNameSegment},
23    condition::ContextCondition,
24    issue::{
25        Issue, IssueExt, IssueSeverity, IssueSource, IssueStage, OptionIssueSource,
26        OptionStyledString, StyledString,
27    },
28    module::Module,
29    source::Source,
30    virtual_source::VirtualSource,
31};
32use turbopack_ecmascript::{
33    EcmascriptParsable,
34    analyzer::{ConstantValue, JsValue, ObjectPart},
35    parse::ParseResult,
36};
37
38use crate::{
39    embed_js::next_js_fs,
40    next_config::{NextConfig, RouteHas},
41    next_import_map::get_next_package,
42    next_manifests::MiddlewareMatcher,
43    next_shared::webpack_rules::WebpackLoaderBuiltinCondition,
44};
45
46const NEXT_TEMPLATE_PATH: &str = "dist/esm/build/templates";
47
48/// As opposed to [`EnvMap`], this map allows for `None` values, which means that the variables
49/// should be replace with undefined.
50#[turbo_tasks::value(transparent)]
51pub struct OptionEnvMap(#[turbo_tasks(trace_ignore)] FxIndexMap<RcStr, Option<RcStr>>);
52
53pub fn defines(define_env: &FxIndexMap<RcStr, Option<RcStr>>) -> CompileTimeDefines {
54    let mut defines = FxIndexMap::default();
55
56    for (k, v) in define_env {
57        defines
58            .entry(
59                k.split('.')
60                    .map(|s| DefinableNameSegment::Name(s.into()))
61                    .collect::<Vec<_>>(),
62            )
63            .or_insert_with(|| {
64                if let Some(v) = v {
65                    let val = serde_json::Value::from_str(v);
66                    match val {
67                        Ok(v) => v.into(),
68                        _ => CompileTimeDefineValue::Evaluate(v.clone()),
69                    }
70                } else {
71                    CompileTimeDefineValue::Undefined
72                }
73            });
74    }
75
76    CompileTimeDefines(defines)
77}
78
79#[derive(
80    Debug, Clone, Copy, PartialEq, Eq, Hash, TaskInput, Serialize, Deserialize, TraceRawVcs,
81)]
82pub enum PathType {
83    PagesPage,
84    PagesApi,
85    Data,
86}
87
88/// Converts a filename within the server root into a next pathname.
89#[turbo_tasks::function]
90pub async fn pathname_for_path(
91    server_root: FileSystemPath,
92    server_path: FileSystemPath,
93    path_ty: PathType,
94) -> Result<Vc<RcStr>> {
95    let server_path_value = server_path.clone();
96    let path = if let Some(path) = server_root.get_path_to(&server_path_value) {
97        path
98    } else {
99        bail!(
100            "server_path ({}) is not in server_root ({})",
101            server_path.value_to_string().await?,
102            server_root.value_to_string().await?
103        )
104    };
105    let path = match (path_ty, path) {
106        // "/" is special-cased to "/index" for data routes.
107        (PathType::Data, "") => rcstr!("/index"),
108        // `get_path_to` always strips the leading `/` from the path, so we need to add
109        // it back here.
110        (_, path) => format!("/{path}").into(),
111    };
112
113    Ok(Vc::cell(path))
114}
115
116// Adapted from https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/get-asset-path-from-route.ts
117// TODO(alexkirsz) There's no need to create an intermediate string here (and
118// below), we should instead return an `impl Display`.
119pub fn get_asset_prefix_from_pathname(pathname: &str) -> String {
120    if pathname == "/" {
121        "/index".to_string()
122    } else if pathname == "/index" || pathname.starts_with("/index/") {
123        format!("/index{pathname}")
124    } else {
125        pathname.to_string()
126    }
127}
128
129// Adapted from https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/get-asset-path-from-route.ts
130pub fn get_asset_path_from_pathname(pathname: &str, ext: &str) -> String {
131    format!("{}{}", get_asset_prefix_from_pathname(pathname), ext)
132}
133
134#[turbo_tasks::function]
135pub async fn get_transpiled_packages(
136    next_config: Vc<NextConfig>,
137    project_path: FileSystemPath,
138) -> Result<Vc<Vec<RcStr>>> {
139    let mut transpile_packages: Vec<RcStr> = next_config.transpile_packages().owned().await?;
140
141    let default_transpiled_packages: Vec<RcStr> = load_next_js_templateon(
142        project_path,
143        rcstr!("dist/lib/default-transpiled-packages.json"),
144    )
145    .await?;
146
147    transpile_packages.extend(default_transpiled_packages.iter().cloned());
148
149    Ok(Vc::cell(transpile_packages))
150}
151
152pub async fn foreign_code_context_condition(
153    next_config: Vc<NextConfig>,
154    project_path: FileSystemPath,
155) -> Result<ContextCondition> {
156    let transpiled_packages = get_transpiled_packages(next_config, project_path.clone()).await?;
157
158    // The next template files are allowed to import the user's code via import
159    // mapping, and imports must use the project-level [ResolveOptions] instead
160    // of the `node_modules` specific resolve options (the template files are
161    // technically node module files).
162    let not_next_template_dir = ContextCondition::not(ContextCondition::InPath(
163        get_next_package(project_path.clone())
164            .await?
165            .join(NEXT_TEMPLATE_PATH)?,
166    ));
167
168    let result = ContextCondition::all(vec![
169        ContextCondition::InDirectory("node_modules".to_string()),
170        not_next_template_dir,
171        ContextCondition::not(ContextCondition::any(
172            transpiled_packages
173                .iter()
174                .map(|package| ContextCondition::InDirectory(format!("node_modules/{package}")))
175                .collect(),
176        )),
177    ]);
178    Ok(result)
179}
180
181/// Determines if the module is an internal asset (i.e overlay, fallback) coming from the embedded
182/// FS, don't apply user defined transforms.
183//
184// TODO: Turbopack specific embed fs paths should be handled by internals of Turbopack itself and
185// user config should not try to leak this. However, currently we apply few transform options
186// subject to Next.js's configuration even if it's embedded assets.
187pub async fn internal_assets_conditions() -> Result<ContextCondition> {
188    Ok(ContextCondition::any(vec![
189        ContextCondition::InPath(next_js_fs().root().owned().await?),
190        ContextCondition::InPath(
191            turbopack_ecmascript_runtime::embed_fs()
192                .root()
193                .owned()
194                .await?,
195        ),
196        ContextCondition::InPath(turbopack_node::embed_js::embed_fs().root().owned().await?),
197    ]))
198}
199
200pub fn app_function_name(page: impl Display) -> String {
201    format!("app{page}")
202}
203pub fn pages_function_name(page: impl Display) -> String {
204    format!("pages{page}")
205}
206
207#[derive(
208    Default,
209    PartialEq,
210    Eq,
211    Clone,
212    Copy,
213    Debug,
214    TraceRawVcs,
215    Serialize,
216    Deserialize,
217    Hash,
218    PartialOrd,
219    Ord,
220    TaskInput,
221    NonLocalValue,
222)]
223#[serde(rename_all = "lowercase")]
224pub enum NextRuntime {
225    #[default]
226    NodeJs,
227    #[serde(alias = "experimental-edge")]
228    Edge,
229}
230
231impl NextRuntime {
232    /// Returns conditions that can be used in the Next.js config's turbopack "rules" section for
233    /// defining webpack loader configuration.
234    pub fn webpack_loader_conditions(&self) -> impl Iterator<Item = WebpackLoaderBuiltinCondition> {
235        match self {
236            NextRuntime::NodeJs => [WebpackLoaderBuiltinCondition::Node],
237            NextRuntime::Edge => [WebpackLoaderBuiltinCondition::EdgeLight],
238        }
239        .into_iter()
240    }
241
242    /// Returns conditions used by `ResolveOptionsContext`.
243    pub fn custom_resolve_conditions(&self) -> impl Iterator<Item = RcStr> {
244        match self {
245            NextRuntime::NodeJs => [rcstr!("node")],
246            NextRuntime::Edge => [rcstr!("edge-light")],
247        }
248        .into_iter()
249    }
250}
251
252#[turbo_tasks::value]
253#[derive(Debug, Clone)]
254pub enum MiddlewareMatcherKind {
255    Str(String),
256    Matcher(MiddlewareMatcher),
257}
258
259#[turbo_tasks::value]
260#[derive(Default, Clone)]
261pub struct NextSourceConfig {
262    pub runtime: NextRuntime,
263
264    /// Middleware router matchers
265    pub matcher: Option<Vec<MiddlewareMatcherKind>>,
266
267    pub regions: Option<Vec<RcStr>>,
268}
269
270#[turbo_tasks::value_impl]
271impl ValueDefault for NextSourceConfig {
272    #[turbo_tasks::function]
273    pub fn value_default() -> Vc<Self> {
274        NextSourceConfig::default().cell()
275    }
276}
277
278/// An issue that occurred while parsing the page config.
279#[turbo_tasks::value(shared)]
280pub struct NextSourceConfigParsingIssue {
281    source: IssueSource,
282    detail: ResolvedVc<StyledString>,
283}
284
285#[turbo_tasks::value_impl]
286impl NextSourceConfigParsingIssue {
287    #[turbo_tasks::function]
288    pub fn new(source: IssueSource, detail: ResolvedVc<StyledString>) -> Vc<Self> {
289        Self { source, detail }.cell()
290    }
291}
292
293#[turbo_tasks::value_impl]
294impl Issue for NextSourceConfigParsingIssue {
295    fn severity(&self) -> IssueSeverity {
296        IssueSeverity::Warning
297    }
298
299    #[turbo_tasks::function]
300    fn title(&self) -> Vc<StyledString> {
301        StyledString::Text(rcstr!(
302            "Next.js can't recognize the exported `config` field in route"
303        ))
304        .cell()
305    }
306
307    #[turbo_tasks::function]
308    fn stage(&self) -> Vc<IssueStage> {
309        IssueStage::Parse.into()
310    }
311
312    #[turbo_tasks::function]
313    fn file_path(&self) -> Vc<FileSystemPath> {
314        self.source.file_path()
315    }
316
317    #[turbo_tasks::function]
318    fn description(&self) -> Vc<OptionStyledString> {
319        Vc::cell(Some(
320            StyledString::Text(
321                "The exported configuration object in a source file need to have a very specific \
322                 format from which some properties can be statically parsed at compiled-time."
323                    .into(),
324            )
325            .resolved_cell(),
326        ))
327    }
328
329    #[turbo_tasks::function]
330    fn detail(&self) -> Vc<OptionStyledString> {
331        Vc::cell(Some(self.detail))
332    }
333
334    #[turbo_tasks::function]
335    fn source(&self) -> Vc<OptionIssueSource> {
336        Vc::cell(Some(self.source))
337    }
338}
339
340async fn emit_invalid_config_warning(
341    source: IssueSource,
342    detail: &str,
343    value: &JsValue,
344) -> Result<()> {
345    let (explainer, hints) = value.explain(2, 0);
346    NextSourceConfigParsingIssue::new(
347        source,
348        StyledString::Text(format!("{detail} Got {explainer}.{hints}").into()).cell(),
349    )
350    .to_resolved()
351    .await?
352    .emit();
353    Ok(())
354}
355
356async fn parse_route_matcher_from_js_value(
357    source: IssueSource,
358    value: &JsValue,
359) -> Result<Option<Vec<MiddlewareMatcherKind>>> {
360    let parse_matcher_kind_matcher = |value: &JsValue| {
361        let mut route_has = vec![];
362        if let JsValue::Array { items, .. } = value {
363            for item in items {
364                if let JsValue::Object { parts, .. } = item {
365                    let mut route_type = None;
366                    let mut route_key = None;
367                    let mut route_value = None;
368
369                    for matcher_part in parts {
370                        if let ObjectPart::KeyValue(part_key, part_value) = matcher_part {
371                            match part_key.as_str() {
372                                Some("type") => {
373                                    route_type = part_value.as_str().map(|v| v.to_string())
374                                }
375                                Some("key") => {
376                                    route_key = part_value.as_str().map(|v| v.to_string())
377                                }
378                                Some("value") => {
379                                    route_value = part_value.as_str().map(|v| v.to_string())
380                                }
381                                _ => {}
382                            }
383                        }
384                    }
385                    let r = match route_type.as_deref() {
386                        Some("header") => route_key.map(|route_key| RouteHas::Header {
387                            key: route_key.into(),
388                            value: route_value.map(From::from),
389                        }),
390                        Some("cookie") => route_key.map(|route_key| RouteHas::Cookie {
391                            key: route_key.into(),
392                            value: route_value.map(From::from),
393                        }),
394                        Some("query") => route_key.map(|route_key| RouteHas::Query {
395                            key: route_key.into(),
396                            value: route_value.map(From::from),
397                        }),
398                        Some("host") => route_value.map(|route_value| RouteHas::Host {
399                            value: route_value.into(),
400                        }),
401                        _ => None,
402                    };
403
404                    if let Some(r) = r {
405                        route_has.push(r);
406                    }
407                }
408            }
409        }
410
411        route_has
412    };
413
414    let mut matchers = vec![];
415
416    match value {
417        JsValue::Constant(matcher) => {
418            if let Some(matcher) = matcher.as_str() {
419                matchers.push(MiddlewareMatcherKind::Str(matcher.to_string()));
420            } else {
421                emit_invalid_config_warning(
422                    source,
423                    "The matcher property must be a string or array of strings",
424                    value,
425                )
426                .await?;
427            }
428        }
429        JsValue::Array { items, .. } => {
430            for item in items {
431                if let Some(matcher) = item.as_str() {
432                    matchers.push(MiddlewareMatcherKind::Str(matcher.to_string()));
433                } else if let JsValue::Object { parts, .. } = item {
434                    let mut matcher = MiddlewareMatcher::default();
435                    for matcher_part in parts {
436                        if let ObjectPart::KeyValue(key, value) = matcher_part {
437                            match key.as_str() {
438                                Some("source") => {
439                                    if let Some(value) = value.as_str() {
440                                        matcher.original_source = value.into();
441                                    }
442                                }
443                                Some("locale") => {
444                                    matcher.locale = value.as_bool().unwrap_or_default();
445                                }
446                                Some("missing") => {
447                                    matcher.missing = Some(parse_matcher_kind_matcher(value))
448                                }
449                                Some("has") => {
450                                    matcher.has = Some(parse_matcher_kind_matcher(value))
451                                }
452                                _ => {
453                                    //noop
454                                }
455                            }
456                        }
457                    }
458
459                    matchers.push(MiddlewareMatcherKind::Matcher(matcher));
460                } else {
461                    emit_invalid_config_warning(
462                        source,
463                        "The matcher property must be a string or array of strings",
464                        value,
465                    )
466                    .await?;
467                }
468            }
469        }
470        _ => {
471            emit_invalid_config_warning(
472                source,
473                "The matcher property must be a string or array of strings",
474                value,
475            )
476            .await?
477        }
478    }
479
480    Ok(if matchers.is_empty() {
481        None
482    } else {
483        Some(matchers)
484    })
485}
486
487#[turbo_tasks::function]
488pub async fn parse_config_from_source(
489    source: ResolvedVc<Box<dyn Source>>,
490    module: ResolvedVc<Box<dyn Module>>,
491    default_runtime: NextRuntime,
492) -> Result<Vc<NextSourceConfig>> {
493    if let Some(ecmascript_asset) = ResolvedVc::try_sidecast::<Box<dyn EcmascriptParsable>>(module)
494        && let ParseResult::Ok {
495            program: Program::Module(module_ast),
496            globals,
497            eval_context,
498            ..
499        } = &*ecmascript_asset.parse_original().await?
500    {
501        for item in &module_ast.body {
502            if let Some(decl) = item
503                .as_module_decl()
504                .and_then(|mod_decl| mod_decl.as_export_decl())
505                .and_then(|export_decl| export_decl.decl.as_var())
506            {
507                for decl in &decl.decls {
508                    let decl_ident = decl.name.as_ident();
509
510                    // Check if there is exported config object `export const config = {...}`
511                    // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
512                    if let Some(ident) = decl_ident
513                        && ident.sym == "config"
514                    {
515                        if let Some(init) = decl.init.as_ref() {
516                            return WrapFuture::new(
517                                async {
518                                    let value = eval_context.eval(init);
519                                    Ok(parse_config_from_js_value(
520                                        IssueSource::from_swc_offsets(
521                                            source,
522                                            init.span_lo().to_u32(),
523                                            init.span_hi().to_u32(),
524                                        ),
525                                        &value,
526                                        default_runtime,
527                                    )
528                                    .await?
529                                    .cell())
530                                },
531                                |f, ctx| GLOBALS.set(globals, || f.poll(ctx)),
532                            )
533                            .await;
534                        } else {
535                            NextSourceConfigParsingIssue::new(
536                                IssueSource::from_swc_offsets(
537                                    source,
538                                    ident.span_lo().to_u32(),
539                                    ident.span_hi().to_u32(),
540                                ),
541                                StyledString::Text(rcstr!(
542                                    "The exported config object must contain an variable \
543                                     initializer."
544                                ))
545                                .cell(),
546                            )
547                            .to_resolved()
548                            .await?
549                            .emit();
550                        }
551                    }
552                    // Or, check if there is segment runtime option
553                    // https://nextjs.org/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes#segment-runtime-Option
554                    else if let Some(ident) = decl_ident
555                        && ident.sym == "runtime"
556                    {
557                        let runtime_value_issue = NextSourceConfigParsingIssue::new(
558                            IssueSource::from_swc_offsets(
559                                source,
560                                ident.span_lo().to_u32(),
561                                ident.span_hi().to_u32(),
562                            ),
563                            StyledString::Text(rcstr!(
564                                "The runtime property must be either \"nodejs\" or \"edge\"."
565                            ))
566                            .cell(),
567                        )
568                        .to_resolved()
569                        .await?;
570                        if let Some(init) = decl.init.as_ref() {
571                            // skipping eval and directly read the expr's value, as we know it
572                            // should be a const string
573                            if let Expr::Lit(Lit::Str(str_value)) = &**init {
574                                let mut config = NextSourceConfig::default();
575
576                                let runtime = &str_value.value;
577                                match runtime.as_str() {
578                                    "edge" | "experimental-edge" => {
579                                        config.runtime = NextRuntime::Edge;
580                                    }
581                                    "nodejs" => {
582                                        config.runtime = NextRuntime::NodeJs;
583                                    }
584                                    _ => {
585                                        runtime_value_issue.emit();
586                                    }
587                                }
588
589                                return Ok(config.cell());
590                            } else {
591                                runtime_value_issue.emit();
592                            }
593                        } else {
594                            NextSourceConfigParsingIssue::new(
595                                IssueSource::from_swc_offsets(
596                                    source,
597                                    ident.span_lo().to_u32(),
598                                    ident.span_hi().to_u32(),
599                                ),
600                                StyledString::Text(rcstr!(
601                                    "The exported segment runtime option must contain an variable \
602                                     initializer."
603                                ))
604                                .cell(),
605                            )
606                            .to_resolved()
607                            .await?
608                            .emit();
609                        }
610                    }
611                }
612            }
613        }
614    }
615    let config = NextSourceConfig {
616        runtime: default_runtime,
617        ..Default::default()
618    };
619
620    Ok(config.cell())
621}
622
623async fn parse_config_from_js_value(
624    source: IssueSource,
625    value: &JsValue,
626    default_runtime: NextRuntime,
627) -> Result<NextSourceConfig> {
628    let mut config = NextSourceConfig {
629        runtime: default_runtime,
630        ..Default::default()
631    };
632
633    if let JsValue::Object { parts, .. } = value {
634        for part in parts {
635            match part {
636                ObjectPart::Spread(_) => {
637                    emit_invalid_config_warning(
638                        source,
639                        "Spread properties are not supported in the config export.",
640                        value,
641                    )
642                    .await?
643                }
644                ObjectPart::KeyValue(key, value) => {
645                    if let Some(key) = key.as_str() {
646                        match key {
647                            "runtime" => {
648                                if let JsValue::Constant(runtime) = value {
649                                    if let Some(runtime) = runtime.as_str() {
650                                        match runtime {
651                                            "edge" | "experimental-edge" => {
652                                                config.runtime = NextRuntime::Edge;
653                                            }
654                                            "nodejs" => {
655                                                config.runtime = NextRuntime::NodeJs;
656                                            }
657                                            _ => {
658                                                emit_invalid_config_warning(
659                                                    source,
660                                                    "The runtime property must be either \
661                                                     \"nodejs\" or \"edge\".",
662                                                    value,
663                                                )
664                                                .await?;
665                                            }
666                                        }
667                                    }
668                                } else {
669                                    emit_invalid_config_warning(
670                                        source,
671                                        "The runtime property must be a constant string.",
672                                        value,
673                                    )
674                                    .await?;
675                                }
676                            }
677                            "matcher" => {
678                                config.matcher =
679                                    parse_route_matcher_from_js_value(source, value).await?;
680                            }
681                            "regions" => {
682                                config.regions = match value {
683                                    // Single value is turned into a single-element Vec.
684                                    JsValue::Constant(ConstantValue::Str(str)) => {
685                                        Some(vec![str.to_string().into()])
686                                    }
687                                    // Array of strings is turned into a Vec. If one of the values
688                                    // in not a String it will
689                                    // error.
690                                    JsValue::Array { items, .. } => {
691                                        let mut regions: Vec<RcStr> = Vec::new();
692                                        for item in items {
693                                            if let JsValue::Constant(ConstantValue::Str(str)) = item
694                                            {
695                                                regions.push(str.to_string().into());
696                                            } else {
697                                                emit_invalid_config_warning(
698                                                    source,
699                                                    "Values of the `config.regions` array need to \
700                                                     static strings",
701                                                    item,
702                                                )
703                                                .await?;
704                                            }
705                                        }
706                                        Some(regions)
707                                    }
708                                    _ => {
709                                        emit_invalid_config_warning(
710                                            source,
711                                            "`config.regions` needs to be a static string or \
712                                             array of static strings",
713                                            value,
714                                        )
715                                        .await?;
716                                        None
717                                    }
718                                };
719                            }
720                            _ => {}
721                        }
722                    } else {
723                        emit_invalid_config_warning(
724                            source,
725                            "The exported config object must not contain non-constant strings.",
726                            key,
727                        )
728                        .await?;
729                    }
730                }
731            }
732        }
733    } else {
734        emit_invalid_config_warning(
735            source,
736            "The exported config object must be a valid object literal.",
737            value,
738        )
739        .await?;
740    }
741
742    Ok(config)
743}
744
745/// Loads a next.js template, replaces `replacements` and `injections` and makes
746/// sure there are none left over.
747pub async fn load_next_js_template(
748    template_path: &str,
749    project_path: FileSystemPath,
750    replacements: &[(&str, &str)],
751    injections: &[(&str, &str)],
752    imports: &[(&str, Option<&str>)],
753) -> Result<Vc<Box<dyn Source>>> {
754    let template_path = virtual_next_js_template_path(project_path.clone(), template_path).await?;
755
756    let content = file_content_rope(template_path.read()).await?;
757    let content = content.to_str()?;
758
759    let package_root = &*get_next_package(project_path).await?;
760
761    let content = expand_next_js_template(
762        &content,
763        &template_path.path,
764        &package_root.path,
765        replacements.iter().copied(),
766        injections.iter().copied(),
767        imports.iter().copied(),
768    )?;
769
770    let file = File::from(content);
771
772    let source = VirtualSource::new(template_path, AssetContent::file(file.into()));
773
774    Ok(Vc::upcast(source))
775}
776
777#[turbo_tasks::function]
778pub async fn file_content_rope(content: Vc<FileContent>) -> Result<Vc<Rope>> {
779    let content = &*content.await?;
780
781    let FileContent::Content(file) = content else {
782        bail!("Expected file content for file");
783    };
784
785    Ok(file.content().to_owned().cell())
786}
787
788async fn virtual_next_js_template_path(
789    project_path: FileSystemPath,
790    file: &str,
791) -> Result<FileSystemPath> {
792    debug_assert!(!file.contains('/'));
793    get_next_package(project_path)
794        .await?
795        .join(&format!("{NEXT_TEMPLATE_PATH}/{file}"))
796}
797
798pub async fn load_next_js_templateon<T: DeserializeOwned>(
799    project_path: FileSystemPath,
800    path: RcStr,
801) -> Result<T> {
802    let file_path = get_next_package(project_path.clone()).await?.join(&path)?;
803
804    let content = &*file_path.read().await?;
805
806    let FileContent::Content(file) = content else {
807        bail!(
808            "Expected file content at {}",
809            file_path.value_to_string().await?
810        );
811    };
812
813    let result: T = parse_json_rope_with_source_context(file.content())?;
814
815    Ok(result)
816}
817
818pub fn styles_rule_condition() -> RuleCondition {
819    RuleCondition::any(vec![
820        RuleCondition::all(vec![
821            RuleCondition::ResourcePathEndsWith(".css".into()),
822            RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.css".into())),
823        ]),
824        RuleCondition::all(vec![
825            RuleCondition::ResourcePathEndsWith(".sass".into()),
826            RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.sass".into())),
827        ]),
828        RuleCondition::all(vec![
829            RuleCondition::ResourcePathEndsWith(".scss".into()),
830            RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.scss".into())),
831        ]),
832        RuleCondition::all(vec![
833            RuleCondition::ContentTypeStartsWith("text/css".into()),
834            RuleCondition::not(RuleCondition::ContentTypeStartsWith(
835                "text/css+module".into(),
836            )),
837        ]),
838        RuleCondition::all(vec![
839            RuleCondition::ContentTypeStartsWith("text/sass".into()),
840            RuleCondition::not(RuleCondition::ContentTypeStartsWith(
841                "text/sass+module".into(),
842            )),
843        ]),
844        RuleCondition::all(vec![
845            RuleCondition::ContentTypeStartsWith("text/scss".into()),
846            RuleCondition::not(RuleCondition::ContentTypeStartsWith(
847                "text/scss+module".into(),
848            )),
849        ]),
850    ])
851}
852pub fn module_styles_rule_condition() -> RuleCondition {
853    RuleCondition::any(vec![
854        RuleCondition::ResourcePathEndsWith(".module.css".into()),
855        RuleCondition::ResourcePathEndsWith(".module.scss".into()),
856        RuleCondition::ResourcePathEndsWith(".module.sass".into()),
857        RuleCondition::ContentTypeStartsWith("text/css+module".into()),
858        RuleCondition::ContentTypeStartsWith("text/sass+module".into()),
859        RuleCondition::ContentTypeStartsWith("text/scss+module".into()),
860    ])
861}