Skip to main content

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