[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:
parent
199ed0ea0f
commit
38ab8397a4
9 changed files with 17973 additions and 21 deletions
|
@ -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" }
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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();
|
||||
|
|
17717
packages/next-swc/crates/next-core/src/next_font_google/__generated__/font-data.json
generated
Normal file
17717
packages/next-swc/crates/next-core/src/next_font_google/__generated__/font-data.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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! {},
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue