next_core/
util.rs

1use std::{fmt::Display, str::FromStr};
2
3use anyhow::{Result, bail};
4use next_taskless::expand_next_js_template;
5use serde::{Deserialize, Serialize, de::DeserializeOwned};
6use turbo_rcstr::{RcStr, rcstr};
7use turbo_tasks::{FxIndexMap, NonLocalValue, TaskInput, Vc, trace::TraceRawVcs};
8use turbo_tasks_fs::{
9    self, File, FileContent, FileSystem, FileSystemPath, json::parse_json_rope_with_source_context,
10    rope::Rope,
11};
12use turbopack::module_options::RuleCondition;
13use turbopack_core::{
14    asset::AssetContent,
15    compile_time_info::{CompileTimeDefineValue, CompileTimeDefines, DefinableNameSegment},
16    condition::ContextCondition,
17    source::Source,
18    virtual_source::VirtualSource,
19};
20
21use crate::{
22    embed_js::next_js_fs, next_config::NextConfig, next_import_map::get_next_package,
23    next_manifests::MiddlewareMatcher, next_shared::webpack_rules::WebpackLoaderBuiltinCondition,
24};
25
26const NEXT_TEMPLATE_PATH: &str = "dist/esm/build/templates";
27
28/// As opposed to [`EnvMap`], this map allows for `None` values, which means that the variables
29/// should be replace with undefined.
30#[turbo_tasks::value(transparent)]
31pub struct OptionEnvMap(#[turbo_tasks(trace_ignore)] FxIndexMap<RcStr, Option<RcStr>>);
32
33pub fn defines(define_env: &FxIndexMap<RcStr, Option<RcStr>>) -> CompileTimeDefines {
34    let mut defines = FxIndexMap::default();
35
36    for (k, v) in define_env {
37        defines
38            .entry(
39                k.split('.')
40                    .map(|s| DefinableNameSegment::Name(s.into()))
41                    .collect::<Vec<_>>(),
42            )
43            .or_insert_with(|| {
44                if let Some(v) = v {
45                    let val = serde_json::Value::from_str(v);
46                    match val {
47                        Ok(v) => v.into(),
48                        _ => CompileTimeDefineValue::Evaluate(v.clone()),
49                    }
50                } else {
51                    CompileTimeDefineValue::Undefined
52                }
53            });
54    }
55
56    CompileTimeDefines(defines)
57}
58
59#[derive(
60    Debug, Clone, Copy, PartialEq, Eq, Hash, TaskInput, Serialize, Deserialize, TraceRawVcs,
61)]
62pub enum PathType {
63    PagesPage,
64    PagesApi,
65    Data,
66}
67
68/// Converts a filename within the server root into a next pathname.
69#[turbo_tasks::function]
70pub async fn pathname_for_path(
71    server_root: FileSystemPath,
72    server_path: FileSystemPath,
73    path_ty: PathType,
74) -> Result<Vc<RcStr>> {
75    let server_path_value = server_path.clone();
76    let path = if let Some(path) = server_root.get_path_to(&server_path_value) {
77        path
78    } else {
79        bail!(
80            "server_path ({}) is not in server_root ({})",
81            server_path.value_to_string().await?,
82            server_root.value_to_string().await?
83        )
84    };
85    let path = match (path_ty, path) {
86        // "/" is special-cased to "/index" for data routes.
87        (PathType::Data, "") => rcstr!("/index"),
88        // `get_path_to` always strips the leading `/` from the path, so we need to add
89        // it back here.
90        (_, path) => format!("/{path}").into(),
91    };
92
93    Ok(Vc::cell(path))
94}
95
96// Adapted from https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/get-asset-path-from-route.ts
97// TODO(alexkirsz) There's no need to create an intermediate string here (and
98// below), we should instead return an `impl Display`.
99pub fn get_asset_prefix_from_pathname(pathname: &str) -> String {
100    if pathname == "/" {
101        "/index".to_string()
102    } else if pathname == "/index" || pathname.starts_with("/index/") {
103        format!("/index{pathname}")
104    } else {
105        pathname.to_string()
106    }
107}
108
109// Adapted from https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/get-asset-path-from-route.ts
110pub fn get_asset_path_from_pathname(pathname: &str, ext: &str) -> String {
111    format!("{}{}", get_asset_prefix_from_pathname(pathname), ext)
112}
113
114#[turbo_tasks::function]
115pub async fn get_transpiled_packages(
116    next_config: Vc<NextConfig>,
117    project_path: FileSystemPath,
118) -> Result<Vc<Vec<RcStr>>> {
119    let mut transpile_packages: Vec<RcStr> = next_config.transpile_packages().owned().await?;
120
121    let default_transpiled_packages: Vec<RcStr> = load_next_js_templateon(
122        project_path,
123        rcstr!("dist/lib/default-transpiled-packages.json"),
124    )
125    .await?;
126
127    transpile_packages.extend(default_transpiled_packages.iter().cloned());
128
129    Ok(Vc::cell(transpile_packages))
130}
131
132pub async fn foreign_code_context_condition(
133    next_config: Vc<NextConfig>,
134    project_path: FileSystemPath,
135) -> Result<ContextCondition> {
136    let transpiled_packages = get_transpiled_packages(next_config, project_path.clone()).await?;
137
138    // The next template files are allowed to import the user's code via import
139    // mapping, and imports must use the project-level [ResolveOptions] instead
140    // of the `node_modules` specific resolve options (the template files are
141    // technically node module files).
142    let not_next_template_dir = ContextCondition::not(ContextCondition::InPath(
143        get_next_package(project_path.clone())
144            .await?
145            .join(NEXT_TEMPLATE_PATH)?,
146    ));
147
148    let result = ContextCondition::all(vec![
149        ContextCondition::InDirectory("node_modules".to_string()),
150        not_next_template_dir,
151        ContextCondition::not(ContextCondition::any(
152            transpiled_packages
153                .iter()
154                .map(|package| ContextCondition::InDirectory(format!("node_modules/{package}")))
155                .collect(),
156        )),
157    ]);
158    Ok(result)
159}
160
161/// Determines if the module is an internal asset (i.e overlay, fallback) coming from the embedded
162/// FS, don't apply user defined transforms.
163//
164// TODO: Turbopack specific embed fs paths should be handled by internals of Turbopack itself and
165// user config should not try to leak this. However, currently we apply few transform options
166// subject to Next.js's configuration even if it's embedded assets.
167pub async fn internal_assets_conditions() -> Result<ContextCondition> {
168    Ok(ContextCondition::any(vec![
169        ContextCondition::InPath(next_js_fs().root().owned().await?),
170        ContextCondition::InPath(
171            turbopack_ecmascript_runtime::embed_fs()
172                .root()
173                .owned()
174                .await?,
175        ),
176        ContextCondition::InPath(turbopack_node::embed_js::embed_fs().root().owned().await?),
177    ]))
178}
179
180pub fn app_function_name(page: impl Display) -> String {
181    format!("app{page}")
182}
183pub fn pages_function_name(page: impl Display) -> String {
184    format!("pages{page}")
185}
186
187#[derive(
188    Default,
189    PartialEq,
190    Eq,
191    Clone,
192    Copy,
193    Debug,
194    TraceRawVcs,
195    Serialize,
196    Deserialize,
197    Hash,
198    PartialOrd,
199    Ord,
200    TaskInput,
201    NonLocalValue,
202)]
203#[serde(rename_all = "lowercase")]
204pub enum NextRuntime {
205    #[default]
206    NodeJs,
207    #[serde(alias = "experimental-edge")]
208    Edge,
209}
210
211impl NextRuntime {
212    /// Returns conditions that can be used in the Next.js config's turbopack "rules" section for
213    /// defining webpack loader configuration.
214    pub fn webpack_loader_conditions(&self) -> impl Iterator<Item = WebpackLoaderBuiltinCondition> {
215        match self {
216            NextRuntime::NodeJs => [WebpackLoaderBuiltinCondition::Node],
217            NextRuntime::Edge => [WebpackLoaderBuiltinCondition::EdgeLight],
218        }
219        .into_iter()
220    }
221
222    /// Returns conditions used by `ResolveOptionsContext`.
223    pub fn custom_resolve_conditions(&self) -> impl Iterator<Item = RcStr> {
224        match self {
225            NextRuntime::NodeJs => [rcstr!("node")],
226            NextRuntime::Edge => [rcstr!("edge-light")],
227        }
228        .into_iter()
229    }
230}
231
232#[derive(PartialEq, Eq, Clone, Debug, TraceRawVcs, Serialize, Deserialize, NonLocalValue)]
233pub enum MiddlewareMatcherKind {
234    Str(String),
235    Matcher(MiddlewareMatcher),
236}
237
238/// Loads a next.js template, replaces `replacements` and `injections` and makes
239/// sure there are none left over.
240pub async fn load_next_js_template(
241    template_path: &str,
242    project_path: FileSystemPath,
243    replacements: &[(&str, &str)],
244    injections: &[(&str, &str)],
245    imports: &[(&str, Option<&str>)],
246) -> Result<Vc<Box<dyn Source>>> {
247    let template_path = virtual_next_js_template_path(project_path.clone(), template_path).await?;
248
249    let content = file_content_rope(template_path.read()).await?;
250    let content = content.to_str()?;
251
252    let package_root = get_next_package(project_path).await?;
253
254    let content = expand_next_js_template(
255        &content,
256        &template_path.path,
257        &package_root.path,
258        replacements.iter().copied(),
259        injections.iter().copied(),
260        imports.iter().copied(),
261    )?;
262
263    let file = File::from(content);
264
265    let source = VirtualSource::new(template_path, AssetContent::file(file.into()));
266
267    Ok(Vc::upcast(source))
268}
269
270#[turbo_tasks::function]
271pub async fn file_content_rope(content: Vc<FileContent>) -> Result<Vc<Rope>> {
272    let content = &*content.await?;
273
274    let FileContent::Content(file) = content else {
275        bail!("Expected file content for file");
276    };
277
278    Ok(file.content().to_owned().cell())
279}
280
281async fn virtual_next_js_template_path(
282    project_path: FileSystemPath,
283    file: &str,
284) -> Result<FileSystemPath> {
285    debug_assert!(!file.contains('/'));
286    get_next_package(project_path)
287        .await?
288        .join(&format!("{NEXT_TEMPLATE_PATH}/{file}"))
289}
290
291pub async fn load_next_js_templateon<T: DeserializeOwned>(
292    project_path: FileSystemPath,
293    path: RcStr,
294) -> Result<T> {
295    let file_path = get_next_package(project_path.clone()).await?.join(&path)?;
296
297    let content = &*file_path.read().await?;
298
299    let FileContent::Content(file) = content else {
300        bail!(
301            "Expected file content at {}",
302            file_path.value_to_string().await?
303        );
304    };
305
306    let result: T = parse_json_rope_with_source_context(file.content())?;
307
308    Ok(result)
309}
310
311pub fn styles_rule_condition() -> RuleCondition {
312    RuleCondition::any(vec![
313        RuleCondition::all(vec![
314            RuleCondition::ResourcePathEndsWith(".css".into()),
315            RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.css".into())),
316        ]),
317        RuleCondition::all(vec![
318            RuleCondition::ResourcePathEndsWith(".sass".into()),
319            RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.sass".into())),
320        ]),
321        RuleCondition::all(vec![
322            RuleCondition::ResourcePathEndsWith(".scss".into()),
323            RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.scss".into())),
324        ]),
325        RuleCondition::all(vec![
326            RuleCondition::ContentTypeStartsWith("text/css".into()),
327            RuleCondition::not(RuleCondition::ContentTypeStartsWith(
328                "text/css+module".into(),
329            )),
330        ]),
331        RuleCondition::all(vec![
332            RuleCondition::ContentTypeStartsWith("text/sass".into()),
333            RuleCondition::not(RuleCondition::ContentTypeStartsWith(
334                "text/sass+module".into(),
335            )),
336        ]),
337        RuleCondition::all(vec![
338            RuleCondition::ContentTypeStartsWith("text/scss".into()),
339            RuleCondition::not(RuleCondition::ContentTypeStartsWith(
340                "text/scss+module".into(),
341            )),
342        ]),
343    ])
344}
345pub fn module_styles_rule_condition() -> RuleCondition {
346    RuleCondition::any(vec![
347        RuleCondition::ResourcePathEndsWith(".module.css".into()),
348        RuleCondition::ResourcePathEndsWith(".module.scss".into()),
349        RuleCondition::ResourcePathEndsWith(".module.sass".into()),
350        RuleCondition::ContentTypeStartsWith("text/css+module".into()),
351        RuleCondition::ContentTypeStartsWith("text/sass+module".into()),
352        RuleCondition::ContentTypeStartsWith("text/scss+module".into()),
353    ])
354}