diff --git a/packages/next-swc/crates/next-build/Cargo.toml b/packages/next-swc/crates/next-build/Cargo.toml index 8d4ab64f93bb..e23d2bae0f33 100644 --- a/packages/next-swc/crates/next-build/Cargo.toml +++ b/packages/next-swc/crates/next-build/Cargo.toml @@ -7,7 +7,6 @@ edition = "2021" autobenches = false [features] -next-font-local = ["next-core/next-font-local"] native-tls = ["next-core/native-tls"] rustls-tls = ["next-core/rustls-tls"] custom_allocator = ["turbo-malloc/custom_allocator"] @@ -24,4 +23,4 @@ turbo-tasks-build = { workspace = true } vergen = { version = "7.3.2", default-features = false, features = [ "cargo", "build", -] } \ No newline at end of file +] } diff --git a/packages/next-swc/crates/next-core/Cargo.toml b/packages/next-swc/crates/next-core/Cargo.toml index eed55ff0d2ba..22c25b655e70 100644 --- a/packages/next-swc/crates/next-core/Cargo.toml +++ b/packages/next-swc/crates/next-core/Cargo.toml @@ -43,7 +43,6 @@ swc_core = { workspace = true, features = ["ecma_ast", "common"] } turbo-tasks-build = { workspace = true } [features] -next-font-local = [] native-tls = ["turbo-tasks-fetch/native-tls"] rustls-tls = ["turbo-tasks-fetch/rustls-tls"] # Internal only. Enabled when building for the Next.js integration test suite. diff --git a/packages/next-swc/crates/next-core/src/next_font/font_fallback.rs b/packages/next-swc/crates/next-core/src/next_font/font_fallback.rs index 517b96c93273..2a4e6bf3c48d 100644 --- a/packages/next-swc/crates/next-core/src/next_font/font_fallback.rs +++ b/packages/next-swc/crates/next-core/src/next_font/font_fallback.rs @@ -34,6 +34,7 @@ pub(crate) struct AutomaticFontFallback { pub adjustment: Option, } +#[derive(Debug)] #[turbo_tasks::value(shared)] pub(crate) enum FontFallback { Automatic(AutomaticFontFallbackVc), @@ -45,7 +46,7 @@ pub(crate) enum FontFallback { } #[turbo_tasks::value(transparent)] -pub(crate) struct FontFallbacks(Vec); +pub(crate) struct FontFallbacks(Vec); #[derive(Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)] pub(crate) struct FontAdjustment { diff --git a/packages/next-swc/crates/next-core/src/next_font/google/mod.rs b/packages/next-swc/crates/next-core/src/next_font/google/mod.rs index d14e8d37b6a8..ca5ce664c3c8 100644 --- a/packages/next-swc/crates/next-core/src/next_font/google/mod.rs +++ b/packages/next-swc/crates/next-core/src/next_font/google/mod.rs @@ -382,7 +382,7 @@ async fn get_mock_stylesheet( ) -> Result> { use std::{collections::HashMap, path::Path}; - use turbo_tasks::{CompletionVc, Value}; + use turbo_tasks::CompletionVc; use turbo_tasks_env::{CommandLineProcessEnvVc, ProcessEnv}; use turbo_tasks_fs::{ json::parse_json_with_source_context, DiskFileSystemVc, File, FileSystem, @@ -418,7 +418,7 @@ async fn get_mock_stylesheet( let ExecutionContext { env, project_path, - intermediate_output_path, + chunking_context, } = *execution_context.await?; let context = node_evaluate_asset_context(project_path, None, None); let loader_path = mock_fs.root().join("loader.js"); @@ -449,7 +449,7 @@ async fn get_mock_stylesheet( env, AssetIdentVc::from_path(loader_path), context, - intermediate_output_path, + chunking_context, None, vec![], CompletionVc::immutable(), diff --git a/packages/next-swc/crates/next-core/src/next_font/google/stylesheet.rs b/packages/next-swc/crates/next-core/src/next_font/google/stylesheet.rs index c66b34ce8101..2c4f44c75cb6 100644 --- a/packages/next-swc/crates/next-core/src/next_font/google/stylesheet.rs +++ b/packages/next-swc/crates/next-core/src/next_font/google/stylesheet.rs @@ -1,9 +1,11 @@ use anyhow::Result; -use indoc::formatdoc; use turbo_tasks::primitives::{OptionStringVc, StringVc}; use super::FontCssPropertiesVc; -use crate::next_font::{font_fallback::FontFallbackVc, stylesheet::build_fallback_definition}; +use crate::next_font::{ + font_fallback::{FontFallbackVc, FontFallbacksVc}, + stylesheet::{build_fallback_definition, build_font_class_rules}, +}; #[turbo_tasks::function] pub(super) async fn build_stylesheet( @@ -15,51 +17,9 @@ pub(super) async fn build_stylesheet( let mut stylesheet = base_stylesheet .as_ref() .map_or_else(|| "".to_owned(), |s| s.to_owned()); - if let Some(definition) = &*build_fallback_definition(font_fallback).await? { - stylesheet.push_str(definition); - } + + stylesheet + .push_str(&build_fallback_definition(FontFallbacksVc::cell(vec![font_fallback])).await?); stylesheet.push_str(&build_font_class_rules(font_css_properties).await?); Ok(StringVc::cell(stylesheet)) } - -#[turbo_tasks::function] -async fn build_font_class_rules(properties: FontCssPropertiesVc) -> Result { - let properties = &*properties.await?; - let font_family = &*properties.font_family.await?; - - let mut result = formatdoc!( - r#" - .className {{ - font-family: {}; - {}{} - }} - "#, - font_family, - properties - .weight - .await? - .as_ref() - .map(|w| format!("font-weight: {};\n", w)) - .unwrap_or_else(|| "".to_owned()), - properties - .style - .await? - .as_ref() - .map(|s| format!("font-style: {};\n", s)) - .unwrap_or_else(|| "".to_owned()), - ); - - if let Some(variable) = &*properties.variable.await? { - result.push_str(&formatdoc!( - r#" - .variable {{ - {}: {}; - }} - "#, - variable, - font_family, - )) - } - - Ok(StringVc::cell(result)) -} diff --git a/packages/next-swc/crates/next-core/src/next_font/local/font_fallback.rs b/packages/next-swc/crates/next-core/src/next_font/local/font_fallback.rs new file mode 100644 index 000000000000..8ba72c849650 --- /dev/null +++ b/packages/next-swc/crates/next-core/src/next_font/local/font_fallback.rs @@ -0,0 +1,54 @@ +use anyhow::Result; +use turbo_tasks::primitives::{StringVc, StringsVc, U32Vc}; + +use super::{options::NextFontLocalOptionsVc, request::AdjustFontFallback}; +use crate::next_font::{ + font_fallback::{AutomaticFontFallback, FontFallback, FontFallbacksVc}, + util::{get_scoped_font_family, FontFamilyType}, +}; + +#[turbo_tasks::function] +pub(super) async fn get_font_fallbacks( + options_vc: NextFontLocalOptionsVc, + request_hash: U32Vc, +) -> Result { + let options = &*options_vc.await?; + let mut font_fallbacks = vec![]; + let scoped_font_family = get_scoped_font_family( + FontFamilyType::Fallback.cell(), + options_vc.font_family(), + request_hash, + ); + + match options.adjust_font_fallback { + AdjustFontFallback::Arial => font_fallbacks.push( + FontFallback::Automatic( + AutomaticFontFallback { + scoped_font_family, + local_font_family: StringVc::cell("Arial".to_owned()), + adjustment: None, + } + .cell(), + ) + .into(), + ), + AdjustFontFallback::TimesNewRoman => font_fallbacks.push( + FontFallback::Automatic( + AutomaticFontFallback { + scoped_font_family, + local_font_family: StringVc::cell("Times New Roman".to_owned()), + adjustment: None, + } + .cell(), + ) + .into(), + ), + AdjustFontFallback::None => (), + }; + + if let Some(fallback) = &options.fallback { + font_fallbacks.push(FontFallback::Manual(StringsVc::cell(fallback.clone())).into()); + } + + Ok(FontFallbacksVc::cell(font_fallbacks)) +} diff --git a/packages/next-swc/crates/next-core/src/next_font/local/mod.rs b/packages/next-swc/crates/next-core/src/next_font/local/mod.rs new file mode 100644 index 000000000000..807865880677 --- /dev/null +++ b/packages/next-swc/crates/next-core/src/next_font/local/mod.rs @@ -0,0 +1,233 @@ +use anyhow::{bail, Context, Result}; +use indoc::formatdoc; +use turbo_tasks::{ + primitives::{OptionStringVc, U32Vc}, + Value, +}; +use turbo_tasks_fs::{json::parse_json_with_source_context, FileContent, FileSystemPathVc}; +use turbopack_core::{ + resolve::{ + options::{ + ImportMapResult, ImportMapResultVc, ImportMapping, ImportMappingReplacement, + ImportMappingReplacementVc, ImportMappingVc, + }, + parse::{Request, RequestVc}, + pattern::QueryMapVc, + ResolveResult, + }, + virtual_asset::VirtualAssetVc, +}; + +use self::{ + font_fallback::get_font_fallbacks, + options::{options_from_request, FontDescriptors, NextFontLocalOptionsVc}, + stylesheet::build_stylesheet, + util::build_font_family_string, +}; +use super::{ + font_fallback::FontFallbacksVc, + util::{FontCssProperties, FontCssPropertiesVc}, +}; +use crate::next_font::{ + local::options::FontWeight, + util::{get_request_hash, get_request_id}, +}; + +pub mod font_fallback; +pub mod options; +pub mod request; +pub mod stylesheet; +pub mod util; + +#[turbo_tasks::value(shared)] +pub(crate) struct NextFontLocalReplacer { + project_path: FileSystemPathVc, +} + +#[turbo_tasks::value_impl] +impl NextFontLocalReplacerVc { + #[turbo_tasks::function] + pub fn new(project_path: FileSystemPathVc) -> Self { + Self::cell(NextFontLocalReplacer { project_path }) + } +} + +#[turbo_tasks::value_impl] +impl ImportMappingReplacement for NextFontLocalReplacer { + #[turbo_tasks::function] + fn replace(&self, _capture: &str) -> ImportMappingVc { + ImportMapping::Ignore.into() + } + + #[turbo_tasks::function] + async fn result( + &self, + context: FileSystemPathVc, + request: RequestVc, + ) -> Result { + let Request::Module { + module: _, + path: _, + query: query_vc + } = &*request.await? else { + return Ok(ImportMapResult::NoEntry.into()); + }; + + let request_hash = get_request_hash(*query_vc); + let options_vc = font_options_from_query_map(*query_vc); + let font_fallbacks = get_font_fallbacks(options_vc, request_hash); + let properties = + &*get_font_css_properties(options_vc, font_fallbacks, request_hash).await?; + let file_content = formatdoc!( + r#" + import cssModule from "@vercel/turbopack-next/internal/font/local/cssmodule.module.css?{}"; + const fontData = {{ + className: cssModule.className, + style: {{ + fontFamily: "{}", + {}{} + }}, + }}; + + if (cssModule.variable != null) {{ + fontData.variable = cssModule.variable; + }} + + export default fontData; + "#, + // Pass along whichever options we received to the css handler + qstring::QString::new(query_vc.await?.as_ref().unwrap().iter().collect()), + properties.font_family.await?, + properties + .weight + .await? + .as_ref() + .map(|w| format!("fontWeight: {},\n", w)) + .unwrap_or_else(|| "".to_owned()), + properties + .style + .await? + .as_ref() + .map(|s| format!("fontStyle: \"{}\",\n", s)) + .unwrap_or_else(|| "".to_owned()), + ); + let js_asset = VirtualAssetVc::new( + context.join(&format!( + "{}.js", + get_request_id(options_vc.font_family(), request_hash).await? + )), + FileContent::Content(file_content.into()).into(), + ); + + Ok(ImportMapResult::Result(ResolveResult::asset(js_asset.into()).into()).into()) + } +} + +#[turbo_tasks::value(shared)] +pub struct NextFontLocalCssModuleReplacer { + project_path: FileSystemPathVc, +} + +#[turbo_tasks::value_impl] +impl NextFontLocalCssModuleReplacerVc { + #[turbo_tasks::function] + pub fn new(project_path: FileSystemPathVc) -> Self { + Self::cell(NextFontLocalCssModuleReplacer { project_path }) + } +} + +#[turbo_tasks::value_impl] +impl ImportMappingReplacement for NextFontLocalCssModuleReplacer { + #[turbo_tasks::function] + fn replace(&self, _capture: &str) -> ImportMappingVc { + ImportMapping::Ignore.into() + } + + #[turbo_tasks::function] + async fn result( + &self, + context: FileSystemPathVc, + request: RequestVc, + ) -> Result { + let request = &*request.await?; + let Request::Module { + module: _, + path: _, + query: query_vc, + } = request else { + return Ok(ImportMapResult::NoEntry.into()); + }; + + let options = font_options_from_query_map(*query_vc); + let request_hash = get_request_hash(*query_vc); + let css_virtual_path = context.join(&format!( + "/{}.module.css", + get_request_id(options.font_family(), request_hash).await? + )); + let fallback = get_font_fallbacks(options, request_hash); + + let stylesheet = build_stylesheet( + font_options_from_query_map(*query_vc), + fallback, + get_font_css_properties(options, fallback, request_hash), + get_request_hash(*query_vc), + ) + .await?; + + let css_asset = VirtualAssetVc::new( + css_virtual_path, + FileContent::Content(stylesheet.into()).into(), + ); + + Ok(ImportMapResult::Result(ResolveResult::asset(css_asset.into()).into()).into()) + } +} + +#[turbo_tasks::function] +async fn get_font_css_properties( + options_vc: NextFontLocalOptionsVc, + font_fallbacks: FontFallbacksVc, + request_hash: U32Vc, +) -> Result { + let options = &*options_vc.await?; + + Ok(FontCssPropertiesVc::cell(FontCssProperties { + font_family: build_font_family_string(options_vc, font_fallbacks, request_hash), + weight: OptionStringVc::cell(match &options.fonts { + FontDescriptors::Many(_) => None, + FontDescriptors::One(descriptor) => descriptor + .weight + .as_ref() + // Don't include values for variable fonts. These are included in font-face + // definitions only. + .filter(|w| !matches!(w, FontWeight::Variable(_, _))) + .map(|w| w.to_string()), + }), + style: OptionStringVc::cell(match &options.fonts { + FontDescriptors::Many(_) => None, + FontDescriptors::One(descriptor) => descriptor.style.clone(), + }), + variable: OptionStringVc::cell(options.variable.clone()), + })) +} + +#[turbo_tasks::function] +async fn font_options_from_query_map(query: QueryMapVc) -> Result { + let query_map = &*query.await?; + // These are invariants from the next/font swc transform. Regular errors instead + // of Issues should be okay. + let query_map = query_map + .as_ref() + .context("next/font/local queries must exist")?; + + if query_map.len() != 1 { + bail!("next/font/local queries must only have one entry"); + } + + let Some((json, _)) = query_map.iter().next() else { + bail!("Expected one entry"); + }; + + options_from_request(&parse_json_with_source_context(json)?) + .map(|o| NextFontLocalOptionsVc::new(Value::new(o))) +} diff --git a/packages/next-swc/crates/next-core/src/next_font/local/options.rs b/packages/next-swc/crates/next-core/src/next_font/local/options.rs new file mode 100644 index 000000000000..982ae0060b1a --- /dev/null +++ b/packages/next-swc/crates/next-core/src/next_font/local/options.rs @@ -0,0 +1,337 @@ +use std::{fmt::Display, str::FromStr}; + +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use turbo_tasks::{primitives::StringVc, trace::TraceRawVcs, Value}; + +use super::request::{ + AdjustFontFallback, NextFontLocalRequest, NextFontLocalRequestArguments, SrcDescriptor, + SrcRequest, +}; + +#[turbo_tasks::value(serialization = "auto_for_input")] +#[derive(Clone, Debug, PartialOrd, Ord, Hash)] +pub(super) struct NextFontLocalOptions { + pub fonts: FontDescriptors, + pub default_weight: Option, + pub default_style: Option, + pub display: String, + pub preload: bool, + pub fallback: Option>, + pub adjust_font_fallback: AdjustFontFallback, + /// An optional name for a css custom property (css variable) that applies + /// the font family when used. + pub variable: Option, + /// The name of the variable assigned to the results of calling the + /// `localFont` function. This is used as the font family's base name. + pub variable_name: String, +} + +#[turbo_tasks::value_impl] +impl NextFontLocalOptionsVc { + #[turbo_tasks::function] + pub fn new(options: Value) -> NextFontLocalOptionsVc { + Self::cell(options.into_value()) + } + + #[turbo_tasks::function] + pub async fn font_family(self) -> Result { + Ok(StringVc::cell((*self.await?.variable_name).to_owned())) + } +} + +#[derive( + Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, TraceRawVcs, +)] +pub(super) struct FontDescriptor { + pub weight: Option, + pub style: Option, + pub path: String, + pub ext: String, +} + +impl FontDescriptor { + fn from_src_request(src_descriptor: &SrcDescriptor) -> Result { + let ext = src_descriptor + .path + .rsplit('.') + .next() + .context("Extension required")? + .to_owned(); + + Ok(Self { + path: src_descriptor.path.to_owned(), + weight: src_descriptor + .weight + .as_ref() + .and_then(|w| FontWeight::from_str(w).ok()), + style: src_descriptor.style.clone(), + ext, + }) + } +} + +#[derive( + Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, TraceRawVcs, +)] +pub(super) enum FontDescriptors { + One(FontDescriptor), + Many(Vec), +} + +#[derive( + Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, Hash, TraceRawVcs, +)] +pub(super) enum FontWeight { + Variable(String, String), + Fixed(String), +} + +pub struct ParseFontWeightErr; +impl FromStr for FontWeight { + type Err = ParseFontWeightErr; + + fn from_str(weight_str: &str) -> std::result::Result { + if let Some((start, end)) = weight_str.split_once(' ') { + Ok(FontWeight::Variable(start.to_owned(), end.to_owned())) + } else { + Ok(FontWeight::Fixed(weight_str.to_owned())) + } + } +} + +impl Display for FontWeight { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Variable(start, end) => format!("{} {}", start, end), + Self::Fixed(val) => val.to_owned(), + } + ) + } +} + +// Transforms the request fields to a validated struct. +// Similar to next/font/local's validateData: +// https://github.com/vercel/next.js/blob/28454c6ddbc310419467e5415aee26e48d079b46/packages/font/src/local/utils.ts#L31 +pub(super) fn options_from_request(request: &NextFontLocalRequest) -> Result { + // Invariant enforced above: either None or Some(the only item in the vec) + let NextFontLocalRequestArguments { + display, + weight, + style, + preload, + fallback, + src, + adjust_font_fallback, + variable, + } = &request.arguments.0; + + let fonts = match src { + SrcRequest::Many(descriptors) => FontDescriptors::Many( + descriptors + .iter() + .map(FontDescriptor::from_src_request) + .collect::>>()?, + ), + SrcRequest::One(path) => { + FontDescriptors::One(FontDescriptor::from_src_request(&SrcDescriptor { + path: path.to_owned(), + weight: weight.to_owned(), + style: style.to_owned(), + })?) + } + }; + + Ok(NextFontLocalOptions { + fonts, + display: display.to_owned(), + preload: preload.to_owned(), + fallback: fallback.to_owned(), + adjust_font_fallback: adjust_font_fallback.to_owned(), + variable: variable.to_owned(), + variable_name: request.variable_name.to_owned(), + default_weight: weight.as_ref().and_then(|s| s.parse().ok()), + default_style: style.to_owned(), + }) +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + use turbo_tasks_fs::json::parse_json_with_source_context; + + use super::{options_from_request, NextFontLocalOptions}; + use crate::next_font::local::{ + options::{FontDescriptor, FontDescriptors, FontWeight}, + request::{AdjustFontFallback, NextFontLocalRequest}, + }; + + #[test] + fn test_uses_defaults() -> Result<()> { + let request: NextFontLocalRequest = parse_json_with_source_context( + r#" + { + "import": "", + "path": "index.js", + "variableName": "myFont", + "arguments": [{ + "src": "./Roboto-Regular.ttf" + }] + } + "#, + )?; + + assert_eq!( + options_from_request(&request)?, + NextFontLocalOptions { + fonts: FontDescriptors::One(FontDescriptor { + path: "./Roboto-Regular.ttf".to_owned(), + weight: None, + style: None, + ext: "ttf".to_owned(), + }), + default_style: None, + default_weight: None, + display: "swap".to_owned(), + preload: true, + fallback: None, + adjust_font_fallback: AdjustFontFallback::Arial, + variable: None, + variable_name: "myFont".to_owned() + }, + ); + + Ok(()) + } + + #[test] + fn test_multiple_src() -> Result<()> { + let request: NextFontLocalRequest = parse_json_with_source_context( + r#" + { + "import": "", + "path": "index.js", + "variableName": "myFont", + "arguments": [{ + "src": [{ + "path": "./Roboto-Regular.ttf", + "weight": "400", + "style": "normal" + }, { + "path": "./Roboto-Italic.ttf", + "weight": "400" + }], + "weight": "300", + "style": "italic" + }] + } + "#, + )?; + + assert_eq!( + options_from_request(&request)?, + NextFontLocalOptions { + fonts: FontDescriptors::Many(vec![ + FontDescriptor { + path: "./Roboto-Regular.ttf".to_owned(), + weight: Some(FontWeight::Fixed("400".to_owned())), + style: Some("normal".to_owned()), + ext: "ttf".to_owned(), + }, + FontDescriptor { + path: "./Roboto-Italic.ttf".to_owned(), + weight: Some(FontWeight::Fixed("400".to_owned())), + style: None, + ext: "ttf".to_owned(), + } + ]), + default_weight: Some(FontWeight::Fixed("300".to_owned())), + default_style: Some("italic".to_owned()), + display: "swap".to_owned(), + preload: true, + fallback: None, + adjust_font_fallback: AdjustFontFallback::Arial, + variable: None, + variable_name: "myFont".to_owned() + }, + ); + + Ok(()) + } + + #[test] + fn test_true_adjust_fallback_fails() -> Result<()> { + let request: Result = parse_json_with_source_context( + r#" + { + "import": "", + "path": "index.js", + "variableName": "myFont", + "arguments": [{ + "src": "./Roboto-Regular.ttf", + "adjustFontFallback": true + }] + } + "#, + ); + + match request { + Ok(r) => panic!("Expected failure, received {:?}", r), + Err(err) => { + assert!(err + .to_string() + .contains("expected Expected string or `false`. Received `true`"),) + } + } + + Ok(()) + } + + #[test] + fn test_specified_options() -> Result<()> { + let request: NextFontLocalRequest = parse_json_with_source_context( + r#" + { + "import": "", + "path": "index.js", + "variableName": "myFont", + "arguments": [{ + "src": "./Roboto-Regular.woff", + "preload": false, + "weight": "500", + "style": "italic", + "fallback": ["Fallback"], + "adjustFontFallback": "Times New Roman", + "display": "optional", + "variable": "myvar" + }] + } + "#, + )?; + + assert_eq!( + options_from_request(&request)?, + NextFontLocalOptions { + fonts: FontDescriptors::One(FontDescriptor { + path: "./Roboto-Regular.woff".to_owned(), + weight: Some(FontWeight::Fixed("500".to_owned())), + style: Some("italic".to_owned()), + ext: "woff".to_owned(), + }), + default_style: Some("italic".to_owned()), + default_weight: Some(FontWeight::Fixed("500".to_owned())), + display: "optional".to_owned(), + preload: false, + fallback: Some(vec!["Fallback".to_owned()]), + adjust_font_fallback: AdjustFontFallback::TimesNewRoman, + variable: Some("myvar".to_owned()), + variable_name: "myFont".to_owned() + }, + ); + + Ok(()) + } +} diff --git a/packages/next-swc/crates/next-core/src/next_font/local/request.rs b/packages/next-swc/crates/next-core/src/next_font/local/request.rs new file mode 100644 index 000000000000..a105dd05fc2e --- /dev/null +++ b/packages/next-swc/crates/next-core/src/next_font/local/request.rs @@ -0,0 +1,167 @@ +use serde::{Deserialize, Serialize}; +use turbo_tasks::trace::TraceRawVcs; + +/// The top-most structure encoded into the query param in requests to +/// `next/font/local` generated by the next/font swc transform. e.g. +/// `next/font/local/target.css?{"path": "index.js", "arguments": {"src":... +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(super) struct NextFontLocalRequest { + pub arguments: (NextFontLocalRequestArguments,), + pub variable_name: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(super) struct NextFontLocalRequestArguments { + pub src: SrcRequest, + pub weight: Option, + pub style: Option, + #[serde(default = "default_display")] + pub display: String, + #[serde(default = "default_preload")] + pub preload: bool, + pub fallback: Option>, + #[serde( + default = "default_adjust_font_fallback", + deserialize_with = "deserialize_adjust_font_fallback" + )] + pub adjust_font_fallback: AdjustFontFallback, + pub variable: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub(super) enum SrcRequest { + One(String), + Many(Vec), +} + +#[derive(Clone, Debug, Deserialize)] +pub(super) struct SrcDescriptor { + pub path: String, + pub weight: Option, + pub style: Option, +} + +#[derive( + Clone, Debug, Deserialize, Hash, Ord, PartialOrd, PartialEq, Eq, Serialize, TraceRawVcs, +)] +pub(super) enum AdjustFontFallback { + Arial, + TimesNewRoman, + None, +} + +fn default_adjust_font_fallback() -> AdjustFontFallback { + AdjustFontFallback::Arial +} + +fn deserialize_adjust_font_fallback<'de, D>( + de: D, +) -> std::result::Result +where + D: serde::Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum AdjustFontFallbackInner { + Named(String), + None(bool), + } + + match AdjustFontFallbackInner::deserialize(de)? { + AdjustFontFallbackInner::Named(name) => match name.as_str() { + "Arial" => Ok(AdjustFontFallback::Arial), + "Times New Roman" => Ok(AdjustFontFallback::TimesNewRoman), + _ => Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Other("adjust_font_fallback"), + &"Expected either \"Arial\" or \"Times New Roman\"", + )), + }, + AdjustFontFallbackInner::None(val) => { + if val { + Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Other("adjust_font_fallback"), + &"Expected string or `false`. Received `true`", + )) + } else { + Ok(AdjustFontFallback::None) + } + } + } +} + +fn default_preload() -> bool { + true +} + +fn default_display() -> String { + "swap".to_owned() +} + +#[cfg(test)] +mod tests { + use super::{default_adjust_font_fallback, deserialize_adjust_font_fallback}; + use anyhow::Result; + use serde::Deserialize; + + use super::AdjustFontFallback; + + #[derive(Debug, Deserialize, PartialEq)] + #[serde(rename_all = "camelCase")] + struct TestFallback { + #[serde( + default = "default_adjust_font_fallback", + deserialize_with = "deserialize_adjust_font_fallback" + )] + pub adjust_font_fallback: AdjustFontFallback, + } + + #[test] + fn test_deserialize_adjust_font_fallback_fails_on_true() { + match serde_json::from_str::(r#"{"adjustFontFallback": true}"#) { + Ok(_) => panic!("Should fail"), + Err(error) => assert!(error.to_string().contains( + "invalid value: adjust_font_fallback, expected Expected string or `false`. \ + Received `true`" + )), + }; + } + + #[test] + fn test_deserialize_adjust_font_fallback_fails_on_unknown_string() { + match serde_json::from_str::(r#"{"adjustFontFallback": "Roboto"}"#) { + Ok(_) => panic!("Should fail"), + Err(error) => assert!( + error.to_string().contains( + r#"invalid value: adjust_font_fallback, expected Expected either "Arial" or "Times New Roman""# + ) + ), + }; + } + + #[test] + fn test_deserializes_false_as_none() -> Result<()> { + assert_eq!( + serde_json::from_str::(r#"{"adjustFontFallback": false}"#)?, + TestFallback { + adjust_font_fallback: AdjustFontFallback::None + } + ); + + Ok(()) + } + + #[test] + fn test_deserializes_arial() -> Result<()> { + assert_eq!( + serde_json::from_str::(r#"{"adjustFontFallback": "Arial"}"#)?, + TestFallback { + adjust_font_fallback: AdjustFontFallback::Arial + } + ); + + Ok(()) + } +} diff --git a/packages/next-swc/crates/next-core/src/next_font/local/stylesheet.rs b/packages/next-swc/crates/next-core/src/next_font/local/stylesheet.rs new file mode 100644 index 000000000000..0408bdce4b55 --- /dev/null +++ b/packages/next-swc/crates/next-core/src/next_font/local/stylesheet.rs @@ -0,0 +1,90 @@ +use anyhow::{bail, Result}; +use indoc::formatdoc; +use turbo_tasks::primitives::{StringVc, U32Vc}; + +use super::options::{FontDescriptors, NextFontLocalOptionsVc}; +use crate::next_font::{ + font_fallback::FontFallbacksVc, + stylesheet::{build_fallback_definition, build_font_class_rules}, + util::{get_scoped_font_family, FontCssPropertiesVc, FontFamilyType}, +}; + +#[turbo_tasks::function] +pub(super) async fn build_stylesheet( + options: NextFontLocalOptionsVc, + fallbacks: FontFallbacksVc, + css_properties: FontCssPropertiesVc, + request_hash: U32Vc, +) -> Result { + let scoped_font_family = get_scoped_font_family( + FontFamilyType::WebFont.cell(), + options.font_family(), + request_hash, + ); + + Ok(StringVc::cell(formatdoc!( + r#" + {} + {} + {} + "#, + *build_font_face_definitions(scoped_font_family, options).await?, + (*build_fallback_definition(fallbacks).await?), + *build_font_class_rules(css_properties).await? + ))) +} + +#[turbo_tasks::function] +pub(super) async fn build_font_face_definitions( + scoped_font_family: StringVc, + options: NextFontLocalOptionsVc, +) -> Result { + let options = &*options.await?; + + let mut definitions = String::new(); + let fonts = match &options.fonts { + FontDescriptors::One(d) => vec![d.clone()], + FontDescriptors::Many(d) => d.clone(), + }; + + for font in fonts { + definitions.push_str(&formatdoc!( + r#" + @font-face {{ + font-family: '{}'; + src: url('{}') format('{}'); + font-display: {}; + {}{} + }} + "#, + *scoped_font_family.await?, + &font.path, + ext_to_format(&font.ext)?, + options.display, + &font + .weight + .as_ref() + .or(options.default_weight.as_ref()) + .map_or_else(|| "".to_owned(), |w| format!("font-weight: {};", w)), + &font + .style + .as_ref() + .or(options.default_style.as_ref()) + .map_or_else(|| "".to_owned(), |s| format!("font-style: {};", s)), + )); + } + + Ok(StringVc::cell(definitions)) +} + +fn ext_to_format(ext: &str) -> Result { + Ok(match ext { + "woff" => "woff", + "woff2" => "woff2", + "ttf" => "truetype", + "otf" => "opentype", + "eot" => "embedded-opentype", + _ => bail!("Unknown font file extension"), + } + .to_owned()) +} diff --git a/packages/next-swc/crates/next-core/src/next_font/local/util.rs b/packages/next-swc/crates/next-core/src/next_font/local/util.rs new file mode 100644 index 000000000000..9a9c83633d06 --- /dev/null +++ b/packages/next-swc/crates/next-core/src/next_font/local/util.rs @@ -0,0 +1,39 @@ +use anyhow::Result; +use turbo_tasks::primitives::{StringVc, U32Vc}; + +use super::options::NextFontLocalOptionsVc; +use crate::next_font::{ + font_fallback::{FontFallback, FontFallbacksVc}, + util::{get_scoped_font_family, FontFamilyType}, +}; + +#[turbo_tasks::function] +pub(super) async fn build_font_family_string( + options: NextFontLocalOptionsVc, + font_fallbacks: FontFallbacksVc, + request_hash: U32Vc, +) -> Result { + let mut font_families = vec![format!( + "'{}'", + *get_scoped_font_family( + FontFamilyType::WebFont.cell(), + options.font_family(), + request_hash, + ) + .await? + )]; + + for font_fallback in &*font_fallbacks.await? { + match *font_fallback.await? { + FontFallback::Automatic(fallback) => { + font_families.push(format!("'{}'", *fallback.await?.scoped_font_family.await?)); + } + FontFallback::Manual(fallbacks) => { + font_families.extend_from_slice(&fallbacks.await?); + } + _ => (), + } + } + + Ok(StringVc::cell(font_families.join(", "))) +} diff --git a/packages/next-swc/crates/next-core/src/next_font/mod.rs b/packages/next-swc/crates/next-core/src/next_font/mod.rs index e82dbe1ff81c..0d77d60a513c 100644 --- a/packages/next-swc/crates/next-core/src/next_font/mod.rs +++ b/packages/next-swc/crates/next-core/src/next_font/mod.rs @@ -1,5 +1,6 @@ pub(crate) mod font_fallback; pub(crate) mod google; pub(crate) mod issue; +pub(crate) mod local; pub(crate) mod stylesheet; pub(crate) mod util; diff --git a/packages/next-swc/crates/next-core/src/next_font/stylesheet.rs b/packages/next-swc/crates/next-core/src/next_font/stylesheet.rs index b6ea6e8d3f02..352335ff37cf 100644 --- a/packages/next-swc/crates/next-core/src/next_font/stylesheet.rs +++ b/packages/next-swc/crates/next-core/src/next_font/stylesheet.rs @@ -1,27 +1,29 @@ use anyhow::Result; use indoc::formatdoc; -use turbo_tasks::primitives::OptionStringVc; +use turbo_tasks::primitives::StringVc; -use super::font_fallback::{FontFallback, FontFallbackVc}; +use super::{ + font_fallback::{FontFallback, FontFallbacksVc}, + util::FontCssPropertiesVc, +}; /// Builds `@font-face` stylesheet definition for a given FontFallback #[turbo_tasks::function] -pub(crate) async fn build_fallback_definition(fallback: FontFallbackVc) -> Result { - Ok(OptionStringVc::cell(match *fallback.await? { - FontFallback::Error => None, - FontFallback::Manual(_) => None, - FontFallback::Automatic(fallback) => { +pub(crate) async fn build_fallback_definition(fallbacks: FontFallbacksVc) -> Result { + let mut res = "".to_owned(); + for fallback_vc in &*fallbacks.await? { + if let FontFallback::Automatic(fallback) = &*fallback_vc.await? { let fallback = fallback.await?; let override_properties = match &fallback.adjustment { None => "".to_owned(), Some(adjustment) => formatdoc!( r#" - ascent-override: {}%; - descent-override: {}%; - line-gap-override: {}%; - size-adjust: {}%; - "#, + ascent-override: {}%; + descent-override: {}%; + line-gap-override: {}%; + size-adjust: {}%; + "#, format_fixed_percentage(adjustment.ascent), format_fixed_percentage(adjustment.descent.abs()), format_fixed_percentage(adjustment.line_gap), @@ -29,20 +31,66 @@ pub(crate) async fn build_fallback_definition(fallback: FontFallbackVc) -> Resul ), }; - Some(formatdoc!( + res.push_str(&formatdoc!( r#" - @font-face {{ - font-family: '{}'; - src: local("{}"); - {} - }} - "#, + @font-face {{ + font-family: '{}'; + src: local("{}"); + {} + }} + "#, fallback.scoped_font_family.await?, fallback.local_font_family.await?, override_properties - )) + )); } - })) + } + + Ok(StringVc::cell(res)) +} + +#[turbo_tasks::function] +pub(super) async fn build_font_class_rules( + css_properties: FontCssPropertiesVc, +) -> Result { + let css_properties = &*css_properties.await?; + let font_family_string = &*css_properties.font_family.await?; + + let mut rules = formatdoc!( + r#" + .className {{ + font-family: {}; + {}{} + }} + "#, + font_family_string, + css_properties + .weight + .await? + .as_ref() + .map(|w| format!("font-weight: {};\n", w)) + .unwrap_or_else(|| "".to_owned()), + css_properties + .style + .await? + .as_ref() + .map(|s| format!("font-style: {};\n", s)) + .unwrap_or_else(|| "".to_owned()), + ); + + if let Some(variable) = &*css_properties.variable.await? { + rules.push_str(&formatdoc!( + r#" + .variable {{ + {}: {}; + }} + "#, + variable, + font_family_string + )) + } + + Ok(StringVc::cell(rules)) } fn format_fixed_percentage(value: f64) -> String { diff --git a/packages/next-swc/crates/next-core/src/next_import_map.rs b/packages/next-swc/crates/next-core/src/next_import_map.rs index b1804c379bea..676fa9338db6 100644 --- a/packages/next-swc/crates/next-core/src/next_import_map.rs +++ b/packages/next-swc/crates/next-core/src/next_import_map.rs @@ -22,7 +22,10 @@ use crate::{ embed_js::{next_js_fs, VIRTUAL_PACKAGE_NAME}, next_client::context::ClientContextType, next_config::NextConfigVc, - next_font::google::{NextFontGoogleCssModuleReplacerVc, NextFontGoogleReplacerVc}, + next_font::{ + google::{NextFontGoogleCssModuleReplacerVc, NextFontGoogleReplacerVc}, + local::{NextFontLocalCssModuleReplacerVc, NextFontLocalReplacerVc}, + }, next_server::context::ServerContextType, }; @@ -395,6 +398,23 @@ pub async fn insert_next_shared_aliases( .into(), ); + import_map.insert_alias( + // Request path from js via next-font swc transform + AliasPattern::exact("next/font/local/target.css"), + ImportMapping::Dynamic(NextFontLocalReplacerVc::new(project_path).into()).into(), + ); + + import_map.insert_alias( + // Request path from js via next-font swc transform + AliasPattern::exact("@next/font/local/target.css"), + ImportMapping::Dynamic(NextFontLocalReplacerVc::new(project_path).into()).into(), + ); + + import_map.insert_alias( + AliasPattern::exact("@vercel/turbopack-next/internal/font/local/cssmodule.module.css"), + ImportMapping::Dynamic(NextFontLocalCssModuleReplacerVc::new(project_path).into()).into(), + ); + import_map.insert_singleton_alias("@swc/helpers", get_next_package(project_path)); import_map.insert_singleton_alias("styled-jsx", get_next_package(project_path)); import_map.insert_singleton_alias("next", project_path); diff --git a/packages/next-swc/crates/next-core/src/next_shared/transforms.rs b/packages/next-swc/crates/next-core/src/next_shared/transforms.rs index a0e266f4fc78..d171a2b99880 100644 --- a/packages/next-swc/crates/next-core/src/next_shared/transforms.rs +++ b/packages/next-swc/crates/next-core/src/next_shared/transforms.rs @@ -121,13 +121,12 @@ impl CustomTransformer for NextJsDynamic { /// Returns a rule which applies the Next.js font transform. pub fn get_next_font_transform_rule() -> ModuleRule { - #[allow(unused_mut)] // This is mutated when next-font-local is enabled - let mut font_loaders = vec!["next/font/google".into(), "@next/font/google".into()]; - #[cfg(feature = "next-font-local")] - { - font_loaders.push("next/font/local".into()); - font_loaders.push("@next/font/local".into()); - } + let font_loaders = vec![ + "next/font/google".into(), + "@next/font/google".into(), + "next/font/local".into(), + "@next/font/local".into(), + ]; let transformer = EcmascriptInputTransform::Custom(CustomTransformVc::cell(box NextJsFont { font_loaders })); diff --git a/packages/next-swc/crates/next-dev/Cargo.toml b/packages/next-swc/crates/next-dev/Cargo.toml index 2c46c1ae81d8..db9447b2cfbe 100644 --- a/packages/next-swc/crates/next-dev/Cargo.toml +++ b/packages/next-swc/crates/next-dev/Cargo.toml @@ -34,7 +34,6 @@ tokio_console = [ ] profile = [] custom_allocator = ["turbo-malloc/custom_allocator"] -next-font-local = ["next-core/next-font-local"] native-tls = ["next-core/native-tls"] rustls-tls = ["next-core/rustls-tls"] # Internal only. Enabled when building for the Next.js integration test suite.