1use std::{fmt::Display, str::FromStr};
2
3use anyhow::{Result, bail};
4use next_taskless::expand_next_js_template;
5use serde::{Deserialize, Serialize, de::DeserializeOwned};
6use turbo_rcstr::{RcStr, rcstr};
7use turbo_tasks::{FxIndexMap, NonLocalValue, TaskInput, Vc, trace::TraceRawVcs};
8use turbo_tasks_fs::{
9 self, File, FileContent, FileSystem, FileSystemPath, json::parse_json_rope_with_source_context,
10 rope::Rope,
11};
12use turbopack::module_options::RuleCondition;
13use turbopack_core::{
14 asset::AssetContent,
15 compile_time_info::{CompileTimeDefineValue, CompileTimeDefines, DefinableNameSegment},
16 condition::ContextCondition,
17 source::Source,
18 virtual_source::VirtualSource,
19};
20
21use crate::{
22 embed_js::next_js_fs, next_config::NextConfig, next_import_map::get_next_package,
23 next_manifests::MiddlewareMatcher, next_shared::webpack_rules::WebpackLoaderBuiltinCondition,
24};
25
26const NEXT_TEMPLATE_PATH: &str = "dist/esm/build/templates";
27
28#[turbo_tasks::value(transparent)]
31pub struct OptionEnvMap(#[turbo_tasks(trace_ignore)] FxIndexMap<RcStr, Option<RcStr>>);
32
33pub fn defines(define_env: &FxIndexMap<RcStr, Option<RcStr>>) -> CompileTimeDefines {
34 let mut defines = FxIndexMap::default();
35
36 for (k, v) in define_env {
37 defines
38 .entry(
39 k.split('.')
40 .map(|s| DefinableNameSegment::Name(s.into()))
41 .collect::<Vec<_>>(),
42 )
43 .or_insert_with(|| {
44 if let Some(v) = v {
45 let val = serde_json::Value::from_str(v);
46 match val {
47 Ok(v) => v.into(),
48 _ => CompileTimeDefineValue::Evaluate(v.clone()),
49 }
50 } else {
51 CompileTimeDefineValue::Undefined
52 }
53 });
54 }
55
56 CompileTimeDefines(defines)
57}
58
59#[derive(
60 Debug, Clone, Copy, PartialEq, Eq, Hash, TaskInput, Serialize, Deserialize, TraceRawVcs,
61)]
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_templateon(
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 Serialize,
196 Deserialize,
197 Hash,
198 PartialOrd,
199 Ord,
200 TaskInput,
201 NonLocalValue,
202)]
203#[serde(rename_all = "lowercase")]
204pub enum NextRuntime {
205 #[default]
206 NodeJs,
207 #[serde(alias = "experimental-edge")]
208 Edge,
209}
210
211impl NextRuntime {
212 pub fn webpack_loader_conditions(&self) -> impl Iterator<Item = WebpackLoaderBuiltinCondition> {
215 match self {
216 NextRuntime::NodeJs => [WebpackLoaderBuiltinCondition::Node],
217 NextRuntime::Edge => [WebpackLoaderBuiltinCondition::EdgeLight],
218 }
219 .into_iter()
220 }
221
222 pub fn custom_resolve_conditions(&self) -> impl Iterator<Item = RcStr> {
224 match self {
225 NextRuntime::NodeJs => [rcstr!("node")],
226 NextRuntime::Edge => [rcstr!("edge-light")],
227 }
228 .into_iter()
229 }
230}
231
232#[derive(PartialEq, Eq, Clone, Debug, TraceRawVcs, Serialize, Deserialize, NonLocalValue)]
233pub enum MiddlewareMatcherKind {
234 Str(String),
235 Matcher(MiddlewareMatcher),
236}
237
238pub async fn load_next_js_template(
241 template_path: &str,
242 project_path: FileSystemPath,
243 replacements: &[(&str, &str)],
244 injections: &[(&str, &str)],
245 imports: &[(&str, Option<&str>)],
246) -> Result<Vc<Box<dyn Source>>> {
247 let template_path = virtual_next_js_template_path(project_path.clone(), template_path).await?;
248
249 let content = file_content_rope(template_path.read()).await?;
250 let content = content.to_str()?;
251
252 let package_root = get_next_package(project_path).await?;
253
254 let content = expand_next_js_template(
255 &content,
256 &template_path.path,
257 &package_root.path,
258 replacements.iter().copied(),
259 injections.iter().copied(),
260 imports.iter().copied(),
261 )?;
262
263 let file = File::from(content);
264
265 let source = VirtualSource::new(template_path, AssetContent::file(file.into()));
266
267 Ok(Vc::upcast(source))
268}
269
270#[turbo_tasks::function]
271pub async fn file_content_rope(content: Vc<FileContent>) -> Result<Vc<Rope>> {
272 let content = &*content.await?;
273
274 let FileContent::Content(file) = content else {
275 bail!("Expected file content for file");
276 };
277
278 Ok(file.content().to_owned().cell())
279}
280
281async fn virtual_next_js_template_path(
282 project_path: FileSystemPath,
283 file: &str,
284) -> Result<FileSystemPath> {
285 debug_assert!(!file.contains('/'));
286 get_next_package(project_path)
287 .await?
288 .join(&format!("{NEXT_TEMPLATE_PATH}/{file}"))
289}
290
291pub async fn load_next_js_templateon<T: DeserializeOwned>(
292 project_path: FileSystemPath,
293 path: RcStr,
294) -> Result<T> {
295 let file_path = get_next_package(project_path.clone()).await?.join(&path)?;
296
297 let content = &*file_path.read().await?;
298
299 let FileContent::Content(file) = content else {
300 bail!(
301 "Expected file content at {}",
302 file_path.value_to_string().await?
303 );
304 };
305
306 let result: T = parse_json_rope_with_source_context(file.content())?;
307
308 Ok(result)
309}
310
311pub fn styles_rule_condition() -> RuleCondition {
312 RuleCondition::any(vec![
313 RuleCondition::all(vec![
314 RuleCondition::ResourcePathEndsWith(".css".into()),
315 RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.css".into())),
316 ]),
317 RuleCondition::all(vec![
318 RuleCondition::ResourcePathEndsWith(".sass".into()),
319 RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.sass".into())),
320 ]),
321 RuleCondition::all(vec![
322 RuleCondition::ResourcePathEndsWith(".scss".into()),
323 RuleCondition::not(RuleCondition::ResourcePathEndsWith(".module.scss".into())),
324 ]),
325 RuleCondition::all(vec![
326 RuleCondition::ContentTypeStartsWith("text/css".into()),
327 RuleCondition::not(RuleCondition::ContentTypeStartsWith(
328 "text/css+module".into(),
329 )),
330 ]),
331 RuleCondition::all(vec![
332 RuleCondition::ContentTypeStartsWith("text/sass".into()),
333 RuleCondition::not(RuleCondition::ContentTypeStartsWith(
334 "text/sass+module".into(),
335 )),
336 ]),
337 RuleCondition::all(vec![
338 RuleCondition::ContentTypeStartsWith("text/scss".into()),
339 RuleCondition::not(RuleCondition::ContentTypeStartsWith(
340 "text/scss+module".into(),
341 )),
342 ]),
343 ])
344}
345pub fn module_styles_rule_condition() -> RuleCondition {
346 RuleCondition::any(vec![
347 RuleCondition::ResourcePathEndsWith(".module.css".into()),
348 RuleCondition::ResourcePathEndsWith(".module.scss".into()),
349 RuleCondition::ResourcePathEndsWith(".module.sass".into()),
350 RuleCondition::ContentTypeStartsWith("text/css+module".into()),
351 RuleCondition::ContentTypeStartsWith("text/sass+module".into()),
352 RuleCondition::ContentTypeStartsWith("text/scss+module".into()),
353 ])
354}