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, 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::{CompileTimeDefineValue, CompileTimeDefines, DefinableNameSegment},
14 condition::ContextCondition,
15 source::Source,
16 virtual_source::VirtualSource,
17};
18
19use crate::{
20 embed_js::next_js_fs, next_config::NextConfig, next_import_map::get_next_package,
21 next_manifests::ProxyMatcher, next_shared::webpack_rules::WebpackLoaderBuiltinCondition,
22};
23
24const NEXT_TEMPLATE_PATH: &str = "dist/esm/build/templates";
25
26#[turbo_tasks::value(transparent)]
29pub struct OptionEnvMap(
30 #[turbo_tasks(trace_ignore)]
31 #[bincode(with = "turbo_bincode::indexmap")]
32 FxIndexMap<RcStr, Option<RcStr>>,
33);
34
35pub fn defines(define_env: &FxIndexMap<RcStr, Option<RcStr>>) -> CompileTimeDefines {
36 let mut defines = FxIndexMap::default();
37
38 for (k, v) in define_env {
39 defines
40 .entry(
41 k.split('.')
42 .map(|s| DefinableNameSegment::Name(s.into()))
43 .collect::<Vec<_>>(),
44 )
45 .or_insert_with(|| {
46 if let Some(v) = v {
47 let val = serde_json::Value::from_str(v);
48 match val {
49 Ok(v) => v.into(),
50 _ => CompileTimeDefineValue::Evaluate(v.clone()),
51 }
52 } else {
53 CompileTimeDefineValue::Undefined
54 }
55 });
56 }
57
58 CompileTimeDefines(defines)
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TaskInput, TraceRawVcs, Encode, Decode)]
62pub enum PathType {
63 PagesPage,
64 PagesApi,
65 Data,
66}
67
68#[turbo_tasks::function]
70pub async fn pathname_for_path(
71 server_root: FileSystemPath,
72 server_path: FileSystemPath,
73 path_ty: PathType,
74) -> Result<Vc<RcStr>> {
75 let server_path_value = server_path.clone();
76 let path = if let Some(path) = server_root.get_path_to(&server_path_value) {
77 path
78 } else {
79 bail!(
80 "server_path ({}) is not in server_root ({})",
81 server_path.value_to_string().await?,
82 server_root.value_to_string().await?
83 )
84 };
85 let path = match (path_ty, path) {
86 (PathType::Data, "") => rcstr!("/index"),
88 (_, path) => format!("/{path}").into(),
91 };
92
93 Ok(Vc::cell(path))
94}
95
96pub fn get_asset_prefix_from_pathname(pathname: &str) -> String {
100 if pathname == "/" {
101 "/index".to_string()
102 } else if pathname == "/index" || pathname.starts_with("/index/") {
103 format!("/index{pathname}")
104 } else {
105 pathname.to_string()
106 }
107}
108
109pub fn get_asset_path_from_pathname(pathname: &str, ext: &str) -> String {
111 format!("{}{}", get_asset_prefix_from_pathname(pathname), ext)
112}
113
114#[turbo_tasks::function]
115pub async fn get_transpiled_packages(
116 next_config: Vc<NextConfig>,
117 project_path: FileSystemPath,
118) -> Result<Vc<Vec<RcStr>>> {
119 let mut transpile_packages: Vec<RcStr> = next_config.transpile_packages().owned().await?;
120
121 let default_transpiled_packages: Vec<RcStr> = load_next_js_json_file(
122 project_path,
123 rcstr!("dist/lib/default-transpiled-packages.json"),
124 )
125 .await?;
126
127 transpile_packages.extend(default_transpiled_packages.iter().cloned());
128
129 Ok(Vc::cell(transpile_packages))
130}
131
132pub async fn foreign_code_context_condition(
133 next_config: Vc<NextConfig>,
134 project_path: FileSystemPath,
135) -> Result<ContextCondition> {
136 let transpiled_packages = get_transpiled_packages(next_config, project_path.clone()).await?;
137
138 let not_next_template_dir = ContextCondition::not(ContextCondition::InPath(
143 get_next_package(project_path.clone())
144 .await?
145 .join(NEXT_TEMPLATE_PATH)?,
146 ));
147
148 let result = ContextCondition::all(vec![
149 ContextCondition::InDirectory("node_modules".to_string()),
150 not_next_template_dir,
151 ContextCondition::not(ContextCondition::any(
152 transpiled_packages
153 .iter()
154 .map(|package| ContextCondition::InDirectory(format!("node_modules/{package}")))
155 .collect(),
156 )),
157 ]);
158 Ok(result)
159}
160
161pub async fn internal_assets_conditions() -> Result<ContextCondition> {
168 Ok(ContextCondition::any(vec![
169 ContextCondition::InPath(next_js_fs().root().owned().await?),
170 ContextCondition::InPath(
171 turbopack_ecmascript_runtime::embed_fs()
172 .root()
173 .owned()
174 .await?,
175 ),
176 ContextCondition::InPath(turbopack_node::embed_js::embed_fs().root().owned().await?),
177 ]))
178}
179
180pub fn app_function_name(page: impl Display) -> String {
181 format!("app{page}")
182}
183pub fn pages_function_name(page: impl Display) -> String {
184 format!("pages{page}")
185}
186
187#[derive(
188 Default,
189 PartialEq,
190 Eq,
191 Clone,
192 Copy,
193 Debug,
194 TraceRawVcs,
195 Deserialize,
196 Hash,
197 PartialOrd,
198 Ord,
199 TaskInput,
200 NonLocalValue,
201 Encode,
202 Decode,
203)]
204#[serde(rename_all = "lowercase")]
205pub enum NextRuntime {
206 #[default]
207 NodeJs,
208 #[serde(alias = "experimental-edge")]
209 Edge,
210}
211
212impl NextRuntime {
213 pub fn webpack_loader_conditions(&self) -> impl Iterator<Item = WebpackLoaderBuiltinCondition> {
216 match self {
217 NextRuntime::NodeJs => [WebpackLoaderBuiltinCondition::Node],
218 NextRuntime::Edge => [WebpackLoaderBuiltinCondition::EdgeLight],
219 }
220 .into_iter()
221 }
222
223 pub fn custom_resolve_conditions(&self) -> impl Iterator<Item = RcStr> {
225 match self {
226 NextRuntime::NodeJs => [rcstr!("node")],
227 NextRuntime::Edge => [rcstr!("edge-light")],
228 }
229 .into_iter()
230 }
231}
232
233#[derive(PartialEq, Eq, Clone, Debug, TraceRawVcs, NonLocalValue, Encode, Decode)]
234pub enum MiddlewareMatcherKind {
235 Str(String),
236 Matcher(ProxyMatcher),
237}
238
239pub async fn load_next_js_template<'b>(
242 template_path: &'b str,
243 project_path: FileSystemPath,
244 replacements: impl IntoIterator<Item = (&'b str, &'b str)>,
245 injections: impl IntoIterator<Item = (&'b str, &'b str)>,
246 imports: impl IntoIterator<Item = (&'b str, Option<&'b str>)>,
247) -> Result<Vc<Box<dyn Source>>> {
248 let template_path = virtual_next_js_template_path(project_path.clone(), template_path).await?;
249
250 let content = file_content_rope(template_path.read()).await?;
251 let content = content.to_str()?;
252
253 let package_root = get_next_package(project_path).await?;
254
255 let content = expand_next_js_template(
256 &content,
257 &template_path.path,
258 &package_root.path,
259 replacements,
260 injections,
261 imports,
262 )?;
263
264 let file = File::from(content);
265 let source = VirtualSource::new(
266 template_path,
267 AssetContent::file(FileContent::Content(file).cell()),
268 );
269
270 Ok(Vc::upcast(source))
271}
272
273pub async fn load_next_js_template_no_imports(
277 template_path: &str,
278 project_path: FileSystemPath,
279 replacements: &[(&str, &str)],
280 injections: &[(&str, &str)],
281 imports: &[(&str, Option<&str>)],
282) -> Result<Vc<Box<dyn Source>>> {
283 let template_path = virtual_next_js_template_path(project_path.clone(), template_path).await?;
284
285 let content = file_content_rope(template_path.read()).await?;
286 let content = content.to_str()?;
287
288 let package_root = get_next_package(project_path).await?;
289
290 let content = expand_next_js_template_no_imports(
291 &content,
292 &template_path.path,
293 &package_root.path,
294 replacements.iter().copied(),
295 injections.iter().copied(),
296 imports.iter().copied(),
297 )?;
298
299 let file = File::from(content);
300 let source = VirtualSource::new(
301 template_path,
302 AssetContent::file(FileContent::Content(file).cell()),
303 );
304
305 Ok(Vc::upcast(source))
306}
307
308#[turbo_tasks::function]
309pub async fn file_content_rope(content: Vc<FileContent>) -> Result<Vc<Rope>> {
310 let content = &*content.await?;
311
312 let FileContent::Content(file) = content else {
313 bail!("Expected file content for file");
314 };
315
316 Ok(file.content().to_owned().cell())
317}
318
319async fn virtual_next_js_template_path(
320 project_path: FileSystemPath,
321 file: &str,
322) -> Result<FileSystemPath> {
323 debug_assert!(!file.contains('/'));
324 get_next_package(project_path)
325 .await?
326 .join(&format!("{NEXT_TEMPLATE_PATH}/{file}"))
327}
328
329pub async fn load_next_js_json_file<T: DeserializeOwned>(
330 project_path: FileSystemPath,
331 sub_path: RcStr,
332) -> Result<T> {
333 let file_path = get_next_package(project_path.clone())
334 .await?
335 .join(&sub_path)?;
336
337 let content = &*file_path.read().await?;
338
339 match content.parse_json_ref() {
340 FileJsonContent::Unparsable(e) => Err(anyhow!("File is not valid JSON: {}", e)),
341 FileJsonContent::NotFound => Err(anyhow!(
342 "File not found: {:?}",
343 file_path.value_to_string().await?
344 )),
345 FileJsonContent::Content(value) => Ok(serde_json::from_value(value)?),
346 }
347}
348
349pub async fn load_next_js_jsonc_file<T: DeserializeOwned>(
350 project_path: FileSystemPath,
351 sub_path: RcStr,
352) -> Result<T> {
353 let file_path = get_next_package(project_path.clone())
354 .await?
355 .join(&sub_path)?;
356
357 let content = &*file_path.read().await?;
358
359 match content.parse_json_with_comments_ref() {
360 FileJsonContent::Unparsable(e) => Err(anyhow!("File is not valid JSON: {}", e)),
361 FileJsonContent::NotFound => Err(anyhow!(
362 "File not found: {:?}",
363 file_path.value_to_string().await?
364 )),
365 FileJsonContent::Content(value) => Ok(serde_json::from_value(value)?),
366 }
367}
368
369pub fn styles_rule_condition() -> RuleCondition {
370 RuleCondition::any(vec![
371 RuleCondition::all(vec![
372 RuleCondition::ResourcePathEndsWith(".css".into()),
373 RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.css".into())),
374 ]),
375 RuleCondition::all(vec![
376 RuleCondition::ResourcePathEndsWith(".sass".into()),
377 RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.sass".into())),
378 ]),
379 RuleCondition::all(vec![
380 RuleCondition::ResourcePathEndsWith(".scss".into()),
381 RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.scss".into())),
382 ]),
383 RuleCondition::all(vec![
384 RuleCondition::ContentTypeStartsWith("text/css".into()),
385 RuleCondition::not(RuleCondition::ContentTypeStartsWith(
386 "text/css+module".into(),
387 )),
388 ]),
389 RuleCondition::all(vec![
390 RuleCondition::ContentTypeStartsWith("text/sass".into()),
391 RuleCondition::not(RuleCondition::ContentTypeStartsWith(
392 "text/sass+module".into(),
393 )),
394 ]),
395 RuleCondition::all(vec![
396 RuleCondition::ContentTypeStartsWith("text/scss".into()),
397 RuleCondition::not(RuleCondition::ContentTypeStartsWith(
398 "text/scss+module".into(),
399 )),
400 ]),
401 ])
402}
403pub fn module_styles_rule_condition() -> RuleCondition {
404 RuleCondition::any(vec![
405 RuleCondition::ResourcePathEndsWith(".module.css".into()),
406 RuleCondition::ResourcePathEndsWith(".module.scss".into()),
407 RuleCondition::ResourcePathEndsWith(".module.sass".into()),
408 RuleCondition::ContentTypeStartsWith("text/css+module".into()),
409 RuleCondition::ContentTypeStartsWith("text/sass+module".into()),
410 RuleCondition::ContentTypeStartsWith("text/scss+module".into()),
411 ])
412}