next_core/
util.rs

1use std::{fmt::Display, str::FromStr};
2
3use anyhow::{Result, anyhow, bail};
4use bincode::{Decode, Encode};
5use next_taskless::{expand_next_js_template, expand_next_js_template_no_imports};
6use serde::{Deserialize, de::DeserializeOwned};
7use turbo_rcstr::{RcStr, rcstr};
8use turbo_tasks::{FxIndexMap, NonLocalValue, TaskInput, Vc, trace::TraceRawVcs};
9use turbo_tasks_fs::{File, FileContent, FileJsonContent, FileSystem, FileSystemPath, rope::Rope};
10use turbopack::module_options::RuleCondition;
11use turbopack_core::{
12    asset::AssetContent,
13    compile_time_info::{CompileTimeDefineValue, CompileTimeDefines, DefinableNameSegment},
14    condition::ContextCondition,
15    source::Source,
16    virtual_source::VirtualSource,
17};
18
19use crate::{
20    embed_js::next_js_fs, next_config::NextConfig, next_import_map::get_next_package,
21    next_manifests::ProxyMatcher, next_shared::webpack_rules::WebpackLoaderBuiltinCondition,
22};
23
24const NEXT_TEMPLATE_PATH: &str = "dist/esm/build/templates";
25
26/// As opposed to [`EnvMap`], this map allows for `None` values, which means that the variables
27/// should be replace with undefined.
28#[turbo_tasks::value(transparent)]
29pub struct OptionEnvMap(
30    #[turbo_tasks(trace_ignore)]
31    #[bincode(with = "turbo_bincode::indexmap")]
32    FxIndexMap<RcStr, Option<RcStr>>,
33);
34
35pub fn defines(define_env: &FxIndexMap<RcStr, Option<RcStr>>) -> CompileTimeDefines {
36    let mut defines = FxIndexMap::default();
37
38    for (k, v) in define_env {
39        defines
40            .entry(
41                k.split('.')
42                    .map(|s| DefinableNameSegment::Name(s.into()))
43                    .collect::<Vec<_>>(),
44            )
45            .or_insert_with(|| {
46                if let Some(v) = v {
47                    let val = serde_json::Value::from_str(v);
48                    match val {
49                        Ok(v) => v.into(),
50                        _ => CompileTimeDefineValue::Evaluate(v.clone()),
51                    }
52                } else {
53                    CompileTimeDefineValue::Undefined
54                }
55            });
56    }
57
58    CompileTimeDefines(defines)
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TaskInput, TraceRawVcs, Encode, Decode)]
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_json_file(
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    Deserialize,
196    Hash,
197    PartialOrd,
198    Ord,
199    TaskInput,
200    NonLocalValue,
201    Encode,
202    Decode,
203)]
204#[serde(rename_all = "lowercase")]
205pub enum NextRuntime {
206    #[default]
207    NodeJs,
208    #[serde(alias = "experimental-edge")]
209    Edge,
210}
211
212impl NextRuntime {
213    /// Returns conditions that can be used in the Next.js config's turbopack "rules" section for
214    /// defining webpack loader configuration.
215    pub fn webpack_loader_conditions(&self) -> impl Iterator<Item = WebpackLoaderBuiltinCondition> {
216        match self {
217            NextRuntime::NodeJs => [WebpackLoaderBuiltinCondition::Node],
218            NextRuntime::Edge => [WebpackLoaderBuiltinCondition::EdgeLight],
219        }
220        .into_iter()
221    }
222
223    /// Returns conditions used by `ResolveOptionsContext`.
224    pub fn custom_resolve_conditions(&self) -> impl Iterator<Item = RcStr> {
225        match self {
226            NextRuntime::NodeJs => [rcstr!("node")],
227            NextRuntime::Edge => [rcstr!("edge-light")],
228        }
229        .into_iter()
230    }
231}
232
233#[derive(PartialEq, Eq, Clone, Debug, TraceRawVcs, NonLocalValue, Encode, Decode)]
234pub enum MiddlewareMatcherKind {
235    Str(String),
236    Matcher(ProxyMatcher),
237}
238
239/// Loads a next.js template, replaces `replacements` and `injections` and makes
240/// sure there are none left over.
241pub async fn load_next_js_template<'b>(
242    template_path: &'b str,
243    project_path: FileSystemPath,
244    replacements: impl IntoIterator<Item = (&'b str, &'b str)>,
245    injections: impl IntoIterator<Item = (&'b str, &'b str)>,
246    imports: impl IntoIterator<Item = (&'b str, Option<&'b str>)>,
247) -> Result<Vc<Box<dyn Source>>> {
248    let template_path = virtual_next_js_template_path(project_path.clone(), template_path).await?;
249
250    let content = file_content_rope(template_path.read()).await?;
251    let content = content.to_str()?;
252
253    let package_root = get_next_package(project_path).await?;
254
255    let content = expand_next_js_template(
256        &content,
257        &template_path.path,
258        &package_root.path,
259        replacements,
260        injections,
261        imports,
262    )?;
263
264    let file = File::from(content);
265    let source = VirtualSource::new(
266        template_path,
267        AssetContent::file(FileContent::Content(file).cell()),
268    );
269
270    Ok(Vc::upcast(source))
271}
272
273/// Loads a next.js template but does **not** require that any relative imports are present
274/// or rewritten. This is intended for small internal templates that do not have their own
275/// imports but still use template variables/injections.
276pub async fn load_next_js_template_no_imports(
277    template_path: &str,
278    project_path: FileSystemPath,
279    replacements: &[(&str, &str)],
280    injections: &[(&str, &str)],
281    imports: &[(&str, Option<&str>)],
282) -> Result<Vc<Box<dyn Source>>> {
283    let template_path = virtual_next_js_template_path(project_path.clone(), template_path).await?;
284
285    let content = file_content_rope(template_path.read()).await?;
286    let content = content.to_str()?;
287
288    let package_root = get_next_package(project_path).await?;
289
290    let content = expand_next_js_template_no_imports(
291        &content,
292        &template_path.path,
293        &package_root.path,
294        replacements.iter().copied(),
295        injections.iter().copied(),
296        imports.iter().copied(),
297    )?;
298
299    let file = File::from(content);
300    let source = VirtualSource::new(
301        template_path,
302        AssetContent::file(FileContent::Content(file).cell()),
303    );
304
305    Ok(Vc::upcast(source))
306}
307
308#[turbo_tasks::function]
309pub async fn file_content_rope(content: Vc<FileContent>) -> Result<Vc<Rope>> {
310    let content = &*content.await?;
311
312    let FileContent::Content(file) = content else {
313        bail!("Expected file content for file");
314    };
315
316    Ok(file.content().to_owned().cell())
317}
318
319async fn virtual_next_js_template_path(
320    project_path: FileSystemPath,
321    file: &str,
322) -> Result<FileSystemPath> {
323    debug_assert!(!file.contains('/'));
324    get_next_package(project_path)
325        .await?
326        .join(&format!("{NEXT_TEMPLATE_PATH}/{file}"))
327}
328
329pub async fn load_next_js_json_file<T: DeserializeOwned>(
330    project_path: FileSystemPath,
331    sub_path: RcStr,
332) -> Result<T> {
333    let file_path = get_next_package(project_path.clone())
334        .await?
335        .join(&sub_path)?;
336
337    let content = &*file_path.read().await?;
338
339    match content.parse_json_ref() {
340        FileJsonContent::Unparsable(e) => Err(anyhow!("File is not valid JSON: {}", e)),
341        FileJsonContent::NotFound => Err(anyhow!(
342            "File not found: {:?}",
343            file_path.value_to_string().await?
344        )),
345        FileJsonContent::Content(value) => Ok(serde_json::from_value(value)?),
346    }
347}
348
349pub async fn load_next_js_jsonc_file<T: DeserializeOwned>(
350    project_path: FileSystemPath,
351    sub_path: RcStr,
352) -> Result<T> {
353    let file_path = get_next_package(project_path.clone())
354        .await?
355        .join(&sub_path)?;
356
357    let content = &*file_path.read().await?;
358
359    match content.parse_json_with_comments_ref() {
360        FileJsonContent::Unparsable(e) => Err(anyhow!("File is not valid JSON: {}", e)),
361        FileJsonContent::NotFound => Err(anyhow!(
362            "File not found: {:?}",
363            file_path.value_to_string().await?
364        )),
365        FileJsonContent::Content(value) => Ok(serde_json::from_value(value)?),
366    }
367}
368
369pub fn styles_rule_condition() -> RuleCondition {
370    RuleCondition::any(vec![
371        RuleCondition::all(vec![
372            RuleCondition::ResourcePathEndsWith(".css".into()),
373            RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.css".into())),
374        ]),
375        RuleCondition::all(vec![
376            RuleCondition::ResourcePathEndsWith(".sass".into()),
377            RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.sass".into())),
378        ]),
379        RuleCondition::all(vec![
380            RuleCondition::ResourcePathEndsWith(".scss".into()),
381            RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.scss".into())),
382        ]),
383        RuleCondition::all(vec![
384            RuleCondition::ContentTypeStartsWith("text/css".into()),
385            RuleCondition::not(RuleCondition::ContentTypeStartsWith(
386                "text/css+module".into(),
387            )),
388        ]),
389        RuleCondition::all(vec![
390            RuleCondition::ContentTypeStartsWith("text/sass".into()),
391            RuleCondition::not(RuleCondition::ContentTypeStartsWith(
392                "text/sass+module".into(),
393            )),
394        ]),
395        RuleCondition::all(vec![
396            RuleCondition::ContentTypeStartsWith("text/scss".into()),
397            RuleCondition::not(RuleCondition::ContentTypeStartsWith(
398                "text/scss+module".into(),
399            )),
400        ]),
401    ])
402}
403pub fn module_styles_rule_condition() -> RuleCondition {
404    RuleCondition::any(vec![
405        RuleCondition::ResourcePathEndsWith(".module.css".into()),
406        RuleCondition::ResourcePathEndsWith(".module.scss".into()),
407        RuleCondition::ResourcePathEndsWith(".module.sass".into()),
408        RuleCondition::ContentTypeStartsWith("text/css+module".into()),
409        RuleCondition::ContentTypeStartsWith("text/sass+module".into()),
410        RuleCondition::ContentTypeStartsWith("text/scss+module".into()),
411    ])
412}