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#[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
63pub fn free_var_references_with_vercel_system_env_warnings(
65 defines: CompileTimeDefines,
66 severity: IssueSeverity,
67) -> FreeVarReferences {
68 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 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#[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 (PathType::Data, "") => rcstr!("/index"),
228 (_, path) => format!("/{path}").into(),
231 };
232
233 Ok(Vc::cell(path))
234}
235
236pub 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
249pub 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 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
301pub 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 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 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
373pub 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
407pub 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
542pub fn worker_forwarded_globals() -> Vec<RcStr> {
546 vec![
547 rcstr!("NEXT_DEPLOYMENT_ID"),
548 rcstr!("NEXT_CLIENT_ASSET_SUFFIX"),
549 ]
550}
551
552pub 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}