next/font/local support for Turbopack (#47369)

Fixes WEB-249

Depends on vercel/turbo#4288

This implements support for `next/font/local` for Turbopack. It:

* Removes the compile-time feature restricting access to `next/font/local`
* Implements `NextFontLocalReplacerVc` and `NextFontLocalCssModuleReplacerVc`, similar to their `next/font/google` equivalents, and adds them as ImportMappings to handle requests for `next/font/local/target.css` and `@vercel/turbopack-next/internal/font/local/cssmodule.module.css` (these requests are created by the JavaScript returned by the requests for the `target.css` module)
* Implements fallback support for Times New Roman and Arial via `adjust_font_fallback` as the webpack implementation does. Font metric override adjustment will be added in a future PR.

Test Plan: New passes 14 next-font integration tests in the Next.js integration test suite, up from 8.
This commit is contained in:
Will Binns-Smith 2023-03-27 09:30:25 -07:00 committed by GitHub
parent 4c2ad81ea7
commit e3875d7421
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 1030 additions and 84 deletions

View file

@ -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",
] }
] }

View file

@ -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.

View file

@ -34,6 +34,7 @@ pub(crate) struct AutomaticFontFallback {
pub adjustment: Option<FontAdjustment>,
}
#[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<FontFallback>);
pub(crate) struct FontFallbacks(Vec<FontFallbackVc>);
#[derive(Debug, PartialEq, Serialize, Deserialize, TraceRawVcs)]
pub(crate) struct FontAdjustment {

View file

@ -382,7 +382,7 @@ async fn get_mock_stylesheet(
) -> Result<Option<String>> {
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(),

View file

@ -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<StringVc> {
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))
}

View file

@ -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<FontFallbacksVc> {
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))
}

View file

@ -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<ImportMapResultVc> {
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<ImportMapResultVc> {
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<FontCssPropertiesVc> {
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<NextFontLocalOptionsVc> {
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)))
}

View file

@ -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<FontWeight>,
pub default_style: Option<String>,
pub display: String,
pub preload: bool,
pub fallback: Option<Vec<String>>,
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<String>,
/// 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<NextFontLocalOptions>) -> NextFontLocalOptionsVc {
Self::cell(options.into_value())
}
#[turbo_tasks::function]
pub async fn font_family(self) -> Result<StringVc> {
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<FontWeight>,
pub style: Option<String>,
pub path: String,
pub ext: String,
}
impl FontDescriptor {
fn from_src_request(src_descriptor: &SrcDescriptor) -> Result<Self> {
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<FontDescriptor>),
}
#[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<Self, Self::Err> {
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<NextFontLocalOptions> {
// 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::<Result<Vec<FontDescriptor>>>()?,
),
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<NextFontLocalRequest> = 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(())
}
}

View file

@ -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<String>,
pub style: Option<String>,
#[serde(default = "default_display")]
pub display: String,
#[serde(default = "default_preload")]
pub preload: bool,
pub fallback: Option<Vec<String>>,
#[serde(
default = "default_adjust_font_fallback",
deserialize_with = "deserialize_adjust_font_fallback"
)]
pub adjust_font_fallback: AdjustFontFallback,
pub variable: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub(super) enum SrcRequest {
One(String),
Many(Vec<SrcDescriptor>),
}
#[derive(Clone, Debug, Deserialize)]
pub(super) struct SrcDescriptor {
pub path: String,
pub weight: Option<String>,
pub style: Option<String>,
}
#[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<AdjustFontFallback, D::Error>
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::<TestFallback>(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::<TestFallback>(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::<TestFallback>(r#"{"adjustFontFallback": false}"#)?,
TestFallback {
adjust_font_fallback: AdjustFontFallback::None
}
);
Ok(())
}
#[test]
fn test_deserializes_arial() -> Result<()> {
assert_eq!(
serde_json::from_str::<TestFallback>(r#"{"adjustFontFallback": "Arial"}"#)?,
TestFallback {
adjust_font_fallback: AdjustFontFallback::Arial
}
);
Ok(())
}
}

View file

@ -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<StringVc> {
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<StringVc> {
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<String> {
Ok(match ext {
"woff" => "woff",
"woff2" => "woff2",
"ttf" => "truetype",
"otf" => "opentype",
"eot" => "embedded-opentype",
_ => bail!("Unknown font file extension"),
}
.to_owned())
}

View file

@ -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<StringVc> {
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(", ")))
}

View file

@ -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;

View file

@ -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<OptionStringVc> {
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<StringVc> {
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<StringVc> {
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 {

View file

@ -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);

View file

@ -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 }));

View file

@ -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.