Skip to main content

next_core/
util.rs

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