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#[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
67pub fn free_var_references_with_vercel_system_env_warnings(
69 defines: CompileTimeDefines,
70 severity: IssueSeverity,
71) -> FreeVarReferences {
72 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 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#[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 (PathType::Data, "") => rcstr!("/index"),
231 (_, path) => format!("/{path}").into(),
234 };
235
236 Ok(Vc::cell(path))
237}
238
239pub 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
252pub 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 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
304pub 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 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 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
382pub 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
416pub 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
551pub fn worker_forwarded_globals() -> Vec<RcStr> {
555 vec![
556 rcstr!("NEXT_DEPLOYMENT_ID"),
557 rcstr!("NEXT_CLIENT_ASSET_SUFFIX"),
558 ]
559}