Skip to main content

next_core/
util.rs

1use std::{fmt::Display, str::FromStr};
2
3use anyhow::{Context, Result, 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, Vc, fxindexset, turbobail};
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::{
14        CompileTimeDefineValue, CompileTimeDefines, DefinableNameSegment, FreeVarReference,
15        FreeVarReferences,
16    },
17    condition::ContextCondition,
18    issue::IssueSeverity,
19    source::Source,
20    virtual_source::VirtualSource,
21};
22
23use crate::{
24    embed_js::next_js_fs, next_config::NextConfig, next_import_map::get_next_package,
25    next_manifests::ProxyMatcher, next_shared::webpack_rules::WebpackLoaderBuiltinCondition,
26};
27
28const NEXT_TEMPLATE_PATH: &str = "dist/esm/build/templates";
29
30/// As opposed to [`EnvMap`], this map allows for `None` values, which means that the variables
31/// should be replace with undefined.
32#[turbo_tasks::value(transparent)]
33pub struct OptionEnvMap(
34    #[bincode(with = "turbo_bincode::indexmap")] FxIndexMap<RcStr, Option<RcStr>>,
35);
36
37pub fn defines(define_env: &FxIndexMap<RcStr, Option<RcStr>>) -> CompileTimeDefines {
38    let mut defines = FxIndexMap::default();
39
40    for (k, v) in define_env {
41        defines
42            .entry(
43                k.split('.')
44                    .map(|s| DefinableNameSegment::Name(s.into()))
45                    .collect::<Vec<_>>(),
46            )
47            .or_insert_with(|| {
48                if let Some(v) = v {
49                    let val = serde_json::Value::from_str(v);
50                    match val {
51                        Ok(v) => v.into(),
52                        _ => CompileTimeDefineValue::Evaluate(v.clone()),
53                    }
54                } else {
55                    CompileTimeDefineValue::Undefined
56                }
57            });
58    }
59
60    CompileTimeDefines(defines)
61}
62
63/// Emits warnings or errors when inlining frequently changing Vercel system env vars
64pub fn free_var_references_with_vercel_system_env_warnings(
65    defines: CompileTimeDefines,
66    severity: IssueSeverity,
67) -> FreeVarReferences {
68    // List of system env vars:
69    //   not available as NEXT_PUBLIC_* anyway:
70    //      CI
71    //      VERCEL
72    //      VERCEL_SKEW_PROTECTION_ENABLED
73    //      VERCEL_AUTOMATION_BYPASS_SECRET
74    //      VERCEL_GIT_PROVIDER
75    //      VERCEL_GIT_REPO_SLUG
76    //      VERCEL_GIT_REPO_OWNER
77    //      VERCEL_GIT_REPO_ID
78    //      VERCEL_OIDC_TOKEN
79    //
80    //   constant:
81    //      VERCEL_PROJECT_PRODUCTION_URL
82    //      VERCEL_REGION
83    //      VERCEL_PROJECT_ID
84    //
85    //   suboptimal (changes production main branch VS preview branches):
86    //      VERCEL_ENV
87    //      VERCEL_TARGET_ENV
88    //
89    //   bad (changes per branch):
90    //      VERCEL_BRANCH_URL
91    //      VERCEL_GIT_COMMIT_REF
92    //      VERCEL_GIT_PULL_REQUEST_ID
93    //
94    //   catastrophic (changes per commit):
95    //      NEXT_DEPLOYMENT_ID
96    //      VERCEL_URL
97    //      VERCEL_DEPLOYMENT_ID
98    //      VERCEL_GIT_COMMIT_SHA
99    //      VERCEL_GIT_COMMIT_MESSAGE
100    //      VERCEL_GIT_COMMIT_AUTHOR_LOGIN
101    //      VERCEL_GIT_COMMIT_AUTHOR_NAME
102    //      VERCEL_GIT_PREVIOUS_SHA
103
104    let entries = defines
105        .0
106        .into_iter()
107        .map(|(k, value)| (k, FreeVarReference::Value(value)));
108
109    fn wrap_report_next_public_usage(
110        public_env_var: &str,
111        inner: Option<Box<FreeVarReference>>,
112        severity: IssueSeverity,
113    ) -> FreeVarReference {
114        let message = match public_env_var {
115            "NEXT_PUBLIC_NEXT_DEPLOYMENT_ID" | "NEXT_PUBLIC_VERCEL_DEPLOYMENT_ID" => {
116                rcstr!(
117                    "The deployment id is being inlined.\nThis variable changes frequently, \
118                     causing slower deploy times and worse browser client-side caching. Use \
119                     `process.env.NEXT_DEPLOYMENT_ID` instead to access the same value without \
120                     inlining, for faster deploy times and better browser client-side caching."
121                )
122            }
123            "NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA" => {
124                rcstr!(
125                    "The commit hash is being inlined.\nThis variable changes frequently, causing \
126                     slower deploy times and worse browser client-side caching. Consider using \
127                     `process.env.NEXT_DEPLOYMENT_ID` to identify a deployment. Alternatively, \
128                     use `process.env.VERCEL_GIT_COMMIT_SHA` in server side code and for browser \
129                     code, remove it."
130                )
131            }
132            "NEXT_PUBLIC_VERCEL_BRANCH_URL" | "NEXT_PUBLIC_VERCEL_URL" => format!(
133                "The deployment url system environment variable is being inlined.\nThis variable \
134                 changes frequently, causing slower deploy times and worse browser client-side \
135                 caching. For server-side code, replace with `process.env.{}` and for browser \
136                 code, read `location.host` instead.",
137                public_env_var.strip_prefix("NEXT_PUBLIC_").unwrap(),
138            )
139            .into(),
140            _ => format!(
141                "A system environment variable is being inlined.\nThis variable changes \
142                 frequently, causing slower deploy times and worse browser client-side caching. \
143                 For server-side code, replace with `process.env.{}` and for browser code, try to \
144                 remove it.",
145                public_env_var.strip_prefix("NEXT_PUBLIC_").unwrap(),
146            )
147            .into(),
148        };
149        FreeVarReference::ReportUsage {
150            message,
151            severity,
152            inner,
153        }
154    }
155
156    let mut list = fxindexset!(
157        "NEXT_PUBLIC_NEXT_DEPLOYMENT_ID",
158        "NEXT_PUBLIC_VERCEL_BRANCH_URL",
159        "NEXT_PUBLIC_VERCEL_DEPLOYMENT_ID",
160        "NEXT_PUBLIC_VERCEL_GIT_COMMIT_AUTHOR_LOGIN",
161        "NEXT_PUBLIC_VERCEL_GIT_COMMIT_AUTHOR_NAME",
162        "NEXT_PUBLIC_VERCEL_GIT_COMMIT_MESSAGE",
163        "NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF",
164        "NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA",
165        "NEXT_PUBLIC_VERCEL_GIT_PREVIOUS_SHA",
166        "NEXT_PUBLIC_VERCEL_GIT_PULL_REQUEST_ID",
167        "NEXT_PUBLIC_VERCEL_URL",
168    );
169
170    let mut entries: FxIndexMap<_, _> = entries
171        .map(|(k, value)| {
172            let value = if let &[
173                DefinableNameSegment::Name(a),
174                DefinableNameSegment::Name(b),
175                DefinableNameSegment::Name(public_env_var),
176            ] = &&*k
177                && a == "process"
178                && b == "env"
179                && list.swap_remove(&**public_env_var)
180            {
181                wrap_report_next_public_usage(public_env_var, Some(Box::new(value)), severity)
182            } else {
183                value
184            };
185            (k, value)
186        })
187        .collect();
188
189    // For the remaining ones, still add a warning, but without replacement
190    for public_env_var in list {
191        entries.insert(
192            vec![
193                rcstr!("process").into(),
194                rcstr!("env").into(),
195                DefinableNameSegment::Name(public_env_var.into()),
196            ],
197            wrap_report_next_public_usage(public_env_var, None, severity),
198        );
199    }
200
201    FreeVarReferences(entries)
202}
203
204#[turbo_tasks::task_input]
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Encode, Decode)]
206pub enum PathType {
207    PagesPage,
208    PagesApi,
209    Data,
210}
211
212/// Converts a filename within the server root into a next pathname.
213#[turbo_tasks::function]
214pub async fn pathname_for_path(
215    server_root: FileSystemPath,
216    server_path: FileSystemPath,
217    path_ty: PathType,
218) -> Result<Vc<RcStr>> {
219    let server_path_value = server_path.clone();
220    let path = if let Some(path) = server_root.get_path_to(&server_path_value) {
221        path
222    } else {
223        turbobail!("server_path ({server_path}) is not in server_root ({server_root})");
224    };
225    let path = match (path_ty, path) {
226        // "/" is special-cased to "/index" for data routes.
227        (PathType::Data, "") => rcstr!("/index"),
228        // `get_path_to` always strips the leading `/` from the path, so we need to add
229        // it back here.
230        (_, path) => format!("/{path}").into(),
231    };
232
233    Ok(Vc::cell(path))
234}
235
236// Adapted from https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/get-asset-path-from-route.ts
237// TODO(alexkirsz) There's no need to create an intermediate string here (and
238// below), we should instead return an `impl Display`.
239pub fn get_asset_prefix_from_pathname(pathname: &str) -> String {
240    if pathname == "/" {
241        "/index".to_string()
242    } else if pathname == "/index" || pathname.starts_with("/index/") {
243        format!("/index{pathname}")
244    } else {
245        pathname.to_string()
246    }
247}
248
249// Adapted from https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/get-asset-path-from-route.ts
250pub fn get_asset_path_from_pathname(pathname: &str, ext: &str) -> String {
251    format!("{}{}", get_asset_prefix_from_pathname(pathname), ext)
252}
253
254#[turbo_tasks::function]
255pub async fn get_transpiled_packages(
256    next_config: Vc<NextConfig>,
257    project_path: FileSystemPath,
258) -> Result<Vc<Vec<RcStr>>> {
259    let mut transpile_packages: Vec<RcStr> = next_config.transpile_packages().owned().await?;
260
261    let default_transpiled_packages: Vec<RcStr> = load_next_js_json_file(
262        project_path,
263        rcstr!("dist/lib/default-transpiled-packages.json"),
264    )
265    .await?;
266
267    transpile_packages.extend(default_transpiled_packages.iter().cloned());
268
269    Ok(Vc::cell(transpile_packages))
270}
271
272pub async fn foreign_code_context_condition(
273    next_config: Vc<NextConfig>,
274    project_path: FileSystemPath,
275) -> Result<ContextCondition> {
276    let transpiled_packages = get_transpiled_packages(next_config, project_path.clone()).await?;
277
278    // The next template files are allowed to import the user's code via import
279    // mapping, and imports must use the project-level [ResolveOptions] instead
280    // of the `node_modules` specific resolve options (the template files are
281    // technically node module files).
282    let not_next_template_dir = ContextCondition::not(ContextCondition::InPath(
283        get_next_package(project_path.clone())
284            .await?
285            .join(NEXT_TEMPLATE_PATH)?,
286    ));
287
288    let result = ContextCondition::all(vec![
289        ContextCondition::InNodeModules,
290        not_next_template_dir,
291        ContextCondition::not(ContextCondition::any(
292            transpiled_packages
293                .iter()
294                .map(|package| ContextCondition::InDirectory(format!("node_modules/{package}")))
295                .collect(),
296        )),
297    ]);
298    Ok(result)
299}
300
301/// Determines if the module is an internal asset (i.e overlay, fallback) coming from the embedded
302/// FS, don't apply user defined transforms.
303//
304// TODO: Turbopack specific embed fs paths should be handled by internals of Turbopack itself and
305// user config should not try to leak this. However, currently we apply few transform options
306// subject to Next.js's configuration even if it's embedded assets.
307pub async fn internal_assets_conditions() -> Result<ContextCondition> {
308    Ok(ContextCondition::any(vec![
309        ContextCondition::InPath(next_js_fs().root().owned().await?),
310        ContextCondition::InPath(
311            turbopack_ecmascript_runtime::embed_fs()
312                .root()
313                .owned()
314                .await?,
315        ),
316        ContextCondition::InPath(turbopack_node::embed_js::embed_fs().root().owned().await?),
317        ContextCondition::InPath(
318            turbopack_ecmascript::embed_js::embed_fs()
319                .root()
320                .owned()
321                .await?,
322        ),
323        ContextCondition::InPath(turbopack_wasm::embed::embed_fs().root().owned().await?),
324    ]))
325}
326
327pub fn app_function_name(page: impl Display) -> String {
328    format!("app{page}")
329}
330pub fn pages_function_name(page: impl Display) -> String {
331    format!("pages{page}")
332}
333
334#[turbo_tasks::task_input]
335#[derive(
336    Default, PartialEq, Eq, Clone, Copy, Debug, Deserialize, Hash, PartialOrd, Ord, Encode, Decode,
337)]
338#[serde(rename_all = "lowercase")]
339pub enum NextRuntime {
340    #[default]
341    NodeJs,
342    #[serde(alias = "experimental-edge")]
343    Edge,
344}
345
346impl NextRuntime {
347    /// Returns conditions that can be used in the Next.js config's turbopack "rules" section for
348    /// defining webpack loader configuration.
349    pub fn webpack_loader_conditions(&self) -> impl Iterator<Item = WebpackLoaderBuiltinCondition> {
350        match self {
351            NextRuntime::NodeJs => [WebpackLoaderBuiltinCondition::Node],
352            NextRuntime::Edge => [WebpackLoaderBuiltinCondition::EdgeLight],
353        }
354        .into_iter()
355    }
356
357    /// Returns conditions used by `ResolveOptionsContext`.
358    pub fn custom_resolve_conditions(&self) -> impl Iterator<Item = RcStr> {
359        match self {
360            NextRuntime::NodeJs => [rcstr!("node")],
361            NextRuntime::Edge => [rcstr!("edge-light")],
362        }
363        .into_iter()
364    }
365}
366
367#[derive(PartialEq, Eq, Clone, Debug, NonLocalValue, Encode, Decode)]
368pub enum MiddlewareMatcherKind {
369    Str(String),
370    Matcher(ProxyMatcher),
371}
372
373/// Loads a next.js template, replaces `replacements` and `injections` and makes
374/// sure there are none left over.
375pub async fn load_next_js_template<'b>(
376    template_path: &'b str,
377    project_path: FileSystemPath,
378    replacements: impl IntoIterator<Item = (&'b str, &'b str)>,
379    injections: impl IntoIterator<Item = (&'b str, &'b str)>,
380    imports: impl IntoIterator<Item = (&'b str, Option<&'b str>)>,
381) -> Result<Vc<Box<dyn Source>>> {
382    let template_path = virtual_next_js_template_path(project_path.clone(), template_path).await?;
383
384    let content = file_content_rope(template_path.read()).await?;
385    let content = content.to_str()?;
386
387    let package_root = get_next_package(project_path).await?;
388
389    let content = expand_next_js_template(
390        &content,
391        &template_path.path,
392        &package_root.path,
393        replacements,
394        injections,
395        imports,
396    )?;
397
398    let file = File::from(content);
399    let source = VirtualSource::new(
400        template_path,
401        AssetContent::file(FileContent::Content(file).cell()),
402    );
403
404    Ok(Vc::upcast(source))
405}
406
407/// Loads a next.js template but does **not** require that any relative imports are present
408/// or rewritten. This is intended for small internal templates that do not have their own
409/// imports but still use template variables/injections.
410pub async fn load_next_js_template_no_imports(
411    template_path: &str,
412    project_path: FileSystemPath,
413    replacements: &[(&str, &str)],
414    injections: &[(&str, &str)],
415    imports: &[(&str, Option<&str>)],
416) -> Result<Vc<Box<dyn Source>>> {
417    let template_path = virtual_next_js_template_path(project_path.clone(), template_path).await?;
418
419    let content = file_content_rope(template_path.read()).await?;
420    let content = content.to_str()?;
421
422    let package_root = get_next_package(project_path).await?;
423
424    let content = expand_next_js_template_no_imports(
425        &content,
426        &template_path.path,
427        &package_root.path,
428        replacements.iter().copied(),
429        injections.iter().copied(),
430        imports.iter().copied(),
431    )?;
432
433    let file = File::from(content);
434    let source = VirtualSource::new(
435        template_path,
436        AssetContent::file(FileContent::Content(file).cell()),
437    );
438
439    Ok(Vc::upcast(source))
440}
441
442#[turbo_tasks::function]
443pub async fn file_content_rope(content: Vc<FileContent>) -> Result<Vc<Rope>> {
444    let content = &*content.await?;
445
446    let FileContent::Content(file) = content else {
447        bail!("Expected file content for file");
448    };
449
450    Ok(file.content().to_owned().cell())
451}
452
453async fn virtual_next_js_template_path(
454    project_path: FileSystemPath,
455    file: &str,
456) -> Result<FileSystemPath> {
457    debug_assert!(!file.contains('/'));
458    get_next_package(project_path)
459        .await?
460        .join(&format!("{NEXT_TEMPLATE_PATH}/{file}"))
461}
462
463pub async fn load_next_js_json_file<T: DeserializeOwned>(
464    project_path: FileSystemPath,
465    sub_path: RcStr,
466) -> Result<T> {
467    let file_path = get_next_package(project_path.clone())
468        .await?
469        .join(&sub_path)?;
470
471    let content = &*file_path.read().await?;
472
473    match content.parse_json_ref() {
474        FileJsonContent::Unparsable(e) => bail!("File is not valid JSON: {e}"),
475        FileJsonContent::NotFound => turbobail!("File not found: {file_path:?}",),
476        FileJsonContent::Content(value) => Ok(serde_json::from_value(value)?),
477    }
478}
479
480pub async fn load_next_js_jsonc_file<T: DeserializeOwned>(
481    project_path: FileSystemPath,
482    sub_path: RcStr,
483) -> Result<T> {
484    let file_path = get_next_package(project_path.clone())
485        .await?
486        .join(&sub_path)?;
487
488    let content = &*file_path.read().await?;
489
490    match content.parse_json_with_comments_ref() {
491        FileJsonContent::Unparsable(e) => turbobail!("File is not valid JSON: {e}"),
492        FileJsonContent::NotFound => turbobail!("File not found: {file_path}",),
493        FileJsonContent::Content(value) => Ok(serde_json::from_value(value)?),
494    }
495}
496
497pub fn styles_rule_condition() -> RuleCondition {
498    RuleCondition::any(vec![
499        RuleCondition::all(vec![
500            RuleCondition::ResourcePathEndsWith(".css".into()),
501            RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.css".into())),
502        ]),
503        RuleCondition::all(vec![
504            RuleCondition::ResourcePathEndsWith(".sass".into()),
505            RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.sass".into())),
506        ]),
507        RuleCondition::all(vec![
508            RuleCondition::ResourcePathEndsWith(".scss".into()),
509            RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.scss".into())),
510        ]),
511        RuleCondition::all(vec![
512            RuleCondition::ContentTypeStartsWith("text/css".into()),
513            RuleCondition::not(RuleCondition::ContentTypeStartsWith(
514                "text/css+module".into(),
515            )),
516        ]),
517        RuleCondition::all(vec![
518            RuleCondition::ContentTypeStartsWith("text/sass".into()),
519            RuleCondition::not(RuleCondition::ContentTypeStartsWith(
520                "text/sass+module".into(),
521            )),
522        ]),
523        RuleCondition::all(vec![
524            RuleCondition::ContentTypeStartsWith("text/scss".into()),
525            RuleCondition::not(RuleCondition::ContentTypeStartsWith(
526                "text/scss+module".into(),
527            )),
528        ]),
529    ])
530}
531pub fn module_styles_rule_condition() -> RuleCondition {
532    RuleCondition::any(vec![
533        RuleCondition::ResourcePathEndsWith(".module.css".into()),
534        RuleCondition::ResourcePathEndsWith(".module.scss".into()),
535        RuleCondition::ResourcePathEndsWith(".module.sass".into()),
536        RuleCondition::ContentTypeStartsWith("text/css+module".into()),
537        RuleCondition::ContentTypeStartsWith("text/sass+module".into()),
538        RuleCondition::ContentTypeStartsWith("text/scss+module".into()),
539    ])
540}
541
542/// Returns the list of global variables that should be forwarded from the main
543/// context to web workers. These are Next.js-specific globals that need to be
544/// available in worker contexts.
545pub fn worker_forwarded_globals() -> Vec<RcStr> {
546    vec![
547        rcstr!("NEXT_DEPLOYMENT_ID"),
548        rcstr!("NEXT_CLIENT_ASSET_SUFFIX"),
549    ]
550}
551
552/// The globs defined in the next.config.mjs are relative to the project root.
553/// The glob walker in turbopack is somewhat naive so we handle relative path directives first so
554/// traversal doesn't need to consider them and can just traverse 'down' the tree.
555/// The main alternative is to merge glob evaluation with directory traversal which is what the npm
556/// `glob` package does, but this would be a substantial rewrite.
557pub fn relativize_glob<'a>(
558    glob: &'a str,
559    relative_to: &FileSystemPath,
560) -> Result<(&'a str, FileSystemPath)> {
561    turbo_tasks_fs::glob::relativize_glob(glob, relative_to).with_context(|| {
562        format!("glob '{glob}' is invalid, it has a prefix that navigates out of the project root")
563    })
564}