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#[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
65pub fn free_var_references_with_vercel_system_env_warnings(
67 defines: CompileTimeDefines,
68 severity: IssueSeverity,
69) -> FreeVarReferences {
70 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 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#[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 (PathType::Data, "") => rcstr!("/index"),
233 (_, path) => format!("/{path}").into(),
236 };
237
238 Ok(Vc::cell(path))
239}
240
241pub 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
254pub 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 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
306pub 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 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 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
384pub 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
418pub 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
559pub fn worker_forwarded_globals() -> Vec<RcStr> {
563 vec![
564 rcstr!("NEXT_DEPLOYMENT_ID"),
565 rcstr!("NEXT_CLIENT_ASSET_SUFFIX"),
566 ]
567}