[6/n] @next/font/google: Cache stylesheet locally (vercel/turbo#2940)

* Add font-data.json

* Initial implementation of dynamic ImportMapping replacement

* Extract font urls

* Download fonts

* Update stylesheet

* Vc-ify

* Apply suggestions from code review

Co-authored-by: Alex Kirszenberg <alex.kirszenberg@vercel.com>

* Check in font-data.json update script and update font-data.json

* Add font update to ci schedule

* assert -> bail

* Remove extraction and caching of fonts

Co-authored-by: Alex Kirszenberg <alex.kirszenberg@vercel.com>
This commit is contained in:
Will Binns-Smith 2022-12-08 08:54:11 -08:00 committed by GitHub
parent 199ed0ea0f
commit 38ab8397a4
9 changed files with 17973 additions and 21 deletions

View file

@ -10,13 +10,19 @@ bench = false
[dependencies]
anyhow = "1.0.47"
auto-hash-map = { path = "../auto-hash-map" }
indexmap = { workspace = true, features = ["serde"] }
indoc = "1.0"
mime = "0.3.16"
once_cell = "1.13.0"
qstring = "0.7.2"
serde = "1.0.136"
serde_json = "1.0.85"
turbo-tasks = { path = "../turbo-tasks" }
turbo-tasks-env = { path = "../turbo-tasks-env" }
turbo-tasks-fetch = { path = "../turbo-tasks-fetch" }
turbo-tasks-fs = { path = "../turbo-tasks-fs" }
turbo-tasks-hash = { path = "../turbo-tasks-hash" }
turbopack = { path = "../turbopack" }
turbopack-core = { path = "../turbopack-core" }
turbopack-dev-server = { path = "../turbopack-dev-server" }

View file

@ -15,7 +15,6 @@ use turbopack_core::{
use turbopack_dev_server::html::DevHtmlAssetVc;
use crate::{
embed_js::attached_next_js_package_path,
next_client::context::{
get_client_chunking_context, get_client_environment, get_client_module_options_context,
get_client_resolve_options_context, get_client_runtime_entries, ContextType,
@ -39,7 +38,7 @@ pub async fn get_fallback_page(
let entries = get_client_runtime_entries(project_root, env, ty);
let mut import_map = ImportMap::empty();
insert_next_shared_aliases(&mut import_map, attached_next_js_package_path(project_root));
insert_next_shared_aliases(&mut import_map, project_root);
let context: AssetContextVc = ModuleAssetContextVc::new(
TransitionsByNameVc::cell(HashMap::new()),

View file

@ -27,6 +27,7 @@ pub use web_entry_source::create_web_entry_source;
pub fn register() {
turbo_tasks::register();
turbo_tasks_fs::register();
turbo_tasks_fetch::register();
turbopack_dev_server::register();
turbopack::register();
turbopack_node::register();

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,213 @@
mod options;
use anyhow::{bail, Context, Result};
use indexmap::IndexMap;
use indoc::formatdoc;
use once_cell::sync::Lazy;
use turbo_tasks::primitives::{OptionStringVc, StringVc};
use turbo_tasks_fetch::fetch;
use turbo_tasks_fs::{FileContent, FileSystemPathVc};
use turbopack_core::{
resolve::{
options::{
ImportMapResult, ImportMapResultVc, ImportMapping, ImportMappingReplacement,
ImportMappingReplacementVc, ImportMappingVc,
},
parse::{Request, RequestVc},
ResolveResult,
},
virtual_asset::VirtualAssetVc,
};
use crate::{
embed_js::attached_next_js_package_path,
next_font_google::{
options::FontDataEntry,
request::NextFontRequest,
util::{get_font_axes, get_stylesheet_url},
},
};
pub(crate) mod options;
pub(crate) mod request;
mod util;
pub const GOOGLE_FONTS_STYLESHEET_URL: &str = "https://fonts.googleapis.com/css2";
static FONT_DATA: Lazy<FontData> =
Lazy::new(|| serde_json::from_str(include_str!("__generated__/font-data.json")).unwrap());
type FontData = IndexMap<String, FontDataEntry>;
#[turbo_tasks::value(shared)]
pub struct NextFontGoogleReplacer {
project_path: FileSystemPathVc,
}
#[turbo_tasks::value_impl]
impl NextFontGoogleReplacerVc {
#[turbo_tasks::function]
pub fn new(project_path: FileSystemPathVc) -> Self {
Self::cell(NextFontGoogleReplacer { project_path })
}
}
#[turbo_tasks::value_impl]
impl ImportMappingReplacement for NextFontGoogleReplacer {
#[turbo_tasks::function]
fn replace(&self, _capture: &str) -> ImportMappingVc {
ImportMapping::Ignore.into()
}
#[turbo_tasks::function]
async fn result(&self, request: RequestVc) -> Result<ImportMapResultVc> {
let request = &*request.await?;
let Request::Module {
module: _,
path: _,
query,
} = request else {
return Ok(ImportMapResult::NoEntry.into());
};
let query = &*query.await?;
let js_asset = VirtualAssetVc::new(
attached_next_js_package_path(self.project_path)
.join("internal/font/google/inter.js"),
FileContent::Content(
formatdoc!(
r#"
import cssModule from "@vercel/turbopack-next/internal/font/google/cssmodule.module.css?{}";
export default {{
className: cssModule.className
}};
"#,
// Pass along whichever options we received to the css handler
qstring::QString::new(query.as_ref().unwrap().iter().collect())
)
.into(),
)
.into(),
);
Ok(ImportMapResult::Result(ResolveResult::Single(js_asset.into(), vec![]).into()).into())
}
}
#[turbo_tasks::value(shared)]
pub struct NextFontGoogleCssModuleReplacer {
project_path: FileSystemPathVc,
}
#[turbo_tasks::value_impl]
impl NextFontGoogleCssModuleReplacerVc {
#[turbo_tasks::function]
pub fn new(project_path: FileSystemPathVc) -> Self {
Self::cell(NextFontGoogleCssModuleReplacer { project_path })
}
}
#[turbo_tasks::value_impl]
impl ImportMappingReplacement for NextFontGoogleCssModuleReplacer {
#[turbo_tasks::function]
fn replace(&self, _capture: &str) -> ImportMappingVc {
ImportMapping::Ignore.into()
}
#[turbo_tasks::function]
async fn result(&self, request: RequestVc) -> Result<ImportMapResultVc> {
let request = &*request.await?;
let Request::Module {
module: _,
path: _,
query,
} = request else {
return Ok(ImportMapResult::NoEntry.into());
};
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/google queries must exist")?;
if query_map.len() != 1 {
bail!("@next/font/google queries must only have one entry");
}
let Some((json, _)) = query_map.iter().next() else {
bail!("Expected one entry");
};
let request: StringVc = StringVc::cell(json.to_owned());
let stylesheet_url = get_stylesheet_url_from_request(request);
// TODO(WEB-274): Handle this failing (e.g. connection issues). This should be
// an Issue.
let stylesheet_res = fetch(
stylesheet_url,
OptionStringVc::cell(Some(
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like \
Gecko) Chrome/104.0.0.0 Safari/537.36"
.to_owned(),
)),
)
.await?;
// TODO(WEB-274): Emit an issue instead
if stylesheet_res.status >= 400 {
bail!("Expected a successful response for Google fonts stylesheet");
}
let stylesheet = &*stylesheet_res.body.to_string().await?;
let options = options_from_request(request).await?;
let css_asset = VirtualAssetVc::new(
attached_next_js_package_path(self.project_path)
.join("internal/font/google/cssmodule.module.css"),
FileContent::Content(
formatdoc!(
r#"
{}
.className {{
font-family: "{}";
}}
"#,
stylesheet,
options.font_family
)
.into(),
)
.into(),
);
Ok(ImportMapResult::Result(ResolveResult::Single(css_asset.into(), vec![]).into()).into())
}
}
#[turbo_tasks::function]
async fn get_stylesheet_url_from_request(request_json: StringVc) -> Result<StringVc> {
let options = options_from_request(request_json).await?;
Ok(StringVc::cell(get_stylesheet_url(
GOOGLE_FONTS_STYLESHEET_URL,
&options.font_family,
&get_font_axes(
&FONT_DATA,
&options.font_family,
&options.weights,
&options.styles,
&options.selected_variable_axes,
)?,
&options.display,
)?))
}
#[turbo_tasks::value(transparent)]
struct NextFontGoogleOptions(self::options::NextFontGoogleOptions);
#[turbo_tasks::function]
async fn options_from_request(request: StringVc) -> Result<NextFontGoogleOptionsVc> {
let request: NextFontRequest = serde_json::from_str(&request.await?)?;
self::options::options_from_request(&request, &FONT_DATA).map(NextFontGoogleOptionsVc::cell)
}

View file

@ -1,15 +1,15 @@
use anyhow::{anyhow, Context, Result};
use indexmap::{indexset, IndexMap, IndexSet};
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use turbo_tasks::trace::TraceRawVcs;
use super::request::{NextFontRequest, OneOrManyStrings};
#[allow(dead_code)]
const ALLOWED_DISPLAY_VALUES: &[&str] = &["auto", "block", "swap", "fallback", "optional"];
pub type FontData = IndexMap<String, FontDataEntry>;
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs)]
pub struct NextFontGoogleOptions {
pub font_family: String,
pub weights: FontWeights,
@ -23,8 +23,7 @@ pub struct NextFontGoogleOptions {
pub subsets: Option<Vec<String>>,
}
#[derive(Debug, PartialEq)]
#[allow(dead_code)]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs)]
pub enum FontWeights {
Variable,
Fixed(IndexSet<String>),
@ -49,7 +48,6 @@ pub struct Axis {
// Transforms the request fields to a struct suitable for making requests to
// Google Fonts. Similar to @next/font/google's validateData:
// https://github.com/vercel/next.js/blob/28454c6ddbc310419467e5415aee26e48d079b46/packages/font/src/google/utils.ts#L22
#[allow(dead_code)]
pub fn options_from_request(
request: &NextFontRequest,
data: &IndexMap<String, FontDataEntry>,

View file

@ -5,8 +5,6 @@ use indexmap::{indexset, IndexSet};
use super::options::{FontData, FontWeights};
const GOOGLE_FONTS_STYLESHEET_URL: &str = "https://fonts.googleapis.com/css2";
#[derive(Debug, PartialEq)]
pub(crate) struct FontAxes {
pub(crate) wght: IndexSet<String>,
@ -21,7 +19,6 @@ pub(crate) enum FontItal {
}
// Derived from https://github.com/vercel/next.js/blob/9e098da0915a2a4581bebe2270953a1216be1ba4/packages/font/src/google/utils.ts#L232
#[allow(dead_code)]
pub(crate) fn get_font_axes(
font_data: &FontData,
font_family: &str,
@ -106,8 +103,8 @@ pub(crate) fn get_font_axes(
}
// Derived from https://github.com/vercel/next.js/blob/9e098da0915a2a4581bebe2270953a1216be1ba4/packages/font/src/google/utils.ts#L128
#[allow(dead_code)]
pub(crate) fn get_stylesheet_url(
root_url: &str,
font_family: &str,
axes: &FontAxes,
display: &str,
@ -204,7 +201,7 @@ pub(crate) fn get_stylesheet_url(
Ok(format!(
"{}?family={}:{}@{}&display={}",
GOOGLE_FONTS_STYLESHEET_URL,
root_url,
font_family.replace(' ', "+"),
variant_keys_str,
variant_values_str,
@ -221,6 +218,7 @@ mod tests {
use crate::next_font_google::{
options::{FontData, FontWeights},
util::{get_stylesheet_url, FontAxes, FontItal},
GOOGLE_FONTS_STYLESHEET_URL,
};
#[test]
@ -371,6 +369,7 @@ mod tests {
fn test_stylesheet_url_no_axes() -> Result<()> {
assert_eq!(
get_stylesheet_url(
GOOGLE_FONTS_STYLESHEET_URL,
"Roboto Mono",
&FontAxes {
wght: indexset! {"500".to_owned()},
@ -389,6 +388,7 @@ mod tests {
fn test_stylesheet_url_sorts_axes() -> Result<()> {
assert_eq!(
get_stylesheet_url(
GOOGLE_FONTS_STYLESHEET_URL,
"Roboto Serif",
&FontAxes {
wght: indexset! {"500".to_owned()},
@ -411,6 +411,7 @@ mod tests {
fn test_stylesheet_url_encodes_all_weight_ital_combinations() -> Result<()> {
assert_eq!(
get_stylesheet_url(
GOOGLE_FONTS_STYLESHEET_URL,
"Roboto Serif",
&FontAxes {
wght: indexset! {"500".to_owned(), "300".to_owned()},
@ -434,6 +435,7 @@ mod tests {
fn test_variable_font_without_wgth_axis() -> Result<()> {
assert_eq!(
get_stylesheet_url(
GOOGLE_FONTS_STYLESHEET_URL,
"Nabla",
&FontAxes {
wght: indexset! {},

View file

@ -1,13 +1,15 @@
use anyhow::Result;
use turbo_tasks::{primitives::StringsVc, Value};
use turbo_tasks_fs::{glob::GlobVc, FileSystemPathVc};
use turbopack_core::resolve::options::{
ImportMap, ImportMapVc, ImportMapping, ImportMappingVc, ResolvedMap, ResolvedMapVc,
use turbopack_core::resolve::{
options::{ImportMap, ImportMapVc, ImportMapping, ImportMappingVc, ResolvedMap, ResolvedMapVc},
AliasPattern,
};
use crate::{
embed_js::{attached_next_js_package_path, VIRTUAL_PACKAGE_NAME},
next_client::context::ContextType,
next_font_google::{NextFontGoogleCssModuleReplacerVc, NextFontGoogleReplacerVc},
next_server::ServerContextType,
};
@ -18,9 +20,8 @@ pub fn get_next_client_import_map(
ty: Value<ContextType>,
) -> ImportMapVc {
let mut import_map = ImportMap::empty();
let package_root = attached_next_js_package_path(project_path);
insert_next_shared_aliases(&mut import_map, package_root);
insert_next_shared_aliases(&mut import_map, project_path);
match ty.into_value() {
ContextType::Pages { pages_dir } => {
@ -102,9 +103,8 @@ pub async fn get_next_server_import_map(
externals: StringsVc,
) -> Result<ImportMapVc> {
let mut import_map = ImportMap::empty();
let package_root = attached_next_js_package_path(project_path);
insert_next_shared_aliases(&mut import_map, package_root);
insert_next_shared_aliases(&mut import_map, project_path);
match ty.into_value() {
ServerContextType::Pages { pages_dir } => {
@ -216,7 +216,9 @@ static NEXT_ALIASES: [(&str, &str); 23] = [
("setImmediate", "next/dist/compiled/setimmediate"),
];
pub fn insert_next_shared_aliases(import_map: &mut ImportMap, package_root: FileSystemPathVc) {
pub fn insert_next_shared_aliases(import_map: &mut ImportMap, project_path: FileSystemPathVc) {
let package_root = attached_next_js_package_path(project_path);
// we use the next.js hydration code, so we replace the error overlay with our
// own
import_map.insert_exact_alias(
@ -229,6 +231,17 @@ pub fn insert_next_shared_aliases(import_map: &mut ImportMap, package_root: File
&format!("{VIRTUAL_PACKAGE_NAME}/"),
package_root,
);
import_map.insert_alias(
// Request path from js via next-font swc transform
AliasPattern::exact("@next/font/google/target.css"),
ImportMapping::Dynamic(NextFontGoogleReplacerVc::new(project_path).into()).into(),
);
import_map.insert_alias(
AliasPattern::exact("@vercel/turbopack-next/internal/font/google/cssmodule.module.css"),
ImportMapping::Dynamic(NextFontGoogleCssModuleReplacerVc::new(project_path).into()).into(),
);
}
/// Inserts an alias to an alternative of import mappings into an import map.

View file

@ -331,6 +331,11 @@ async fn source(
.into();
let static_source =
StaticAssetsContentSourceVc::new(String::new(), project_path.join("public")).into();
let next_static_source = StaticAssetsContentSourceVc::new(
"_next/static/".to_owned(),
project_path.join(".next/static"),
)
.into();
let manifest_source = DevManifestContentSource {
page_roots: vec![app_source, rendered_source],
}
@ -338,6 +343,7 @@ async fn source(
.into();
let main_source = CombinedContentSourceVc::new(vec![
manifest_source,
next_static_source,
static_source,
app_source,
rendered_source,