@@ -14,6 +14,7 @@ import type {
1414 SystemFallbacksProvider ,
1515 UrlProxy ,
1616} from './definitions.js' ;
17+ import { dedupeFontFaces } from './logic/dedupe-font-faces.js' ;
1718import { extractUnifontProviders } from './logic/extract-unifont-providers.js' ;
1819import { normalizeRemoteFontFaces } from './logic/normalize-remote-font-faces.js' ;
1920import { type CollectedFontForMetrics , optimizeFallbacks } from './logic/optimize-fallbacks.js' ;
@@ -28,6 +29,7 @@ import type {
2829 FontFileDataMap ,
2930 InternalConsumableMap ,
3031 PreloadData ,
32+ ResolvedFontFamily ,
3133} from './types.js' ;
3234import {
3335 pickFontFaceProperty ,
@@ -122,16 +124,50 @@ export async function orchestrate({
122124 */
123125 const consumableMap : ConsumableMap = new Map ( ) ;
124126
125- for ( const family of resolvedFamilies ) {
126- const preloadData : Array < PreloadData > = [ ] ;
127- const consumableMapValue : Array < FontData > = [ ] ;
128- let css = '' ;
127+ /**
128+ * Holds family data by a key, to allow merging families
129+ */
130+ const resolvedFamiliesMap = new Map <
131+ string ,
132+ {
133+ family : ResolvedFontFamily ;
134+ fonts : Array < unifont . FontFaceData > ;
135+ fallbacks : Array < string > ;
136+ /**
137+ * Holds a list of font files to be used for optimized fallbacks generation
138+ */
139+ collectedFonts : Array < CollectedFontForMetrics > ;
140+ preloadData : Array < PreloadData > ;
141+ }
142+ > ( ) ;
129143
130- /**
131- * Holds a list of font files to be used for optimized fallbacks generation
132- */
133- const collectedFonts : Array < CollectedFontForMetrics > = [ ] ;
134- const fallbacks = family . fallbacks ?? defaults . fallbacks ?? [ ] ;
144+ // First loop: we try to merge families. This is useful for advanced cases, where eg. you want
145+ // 500, 600, 700 as normal but also 500 as italic. That requires 2 families
146+ for ( const family of resolvedFamilies ) {
147+ const key = `${ family . cssVariable } :${ family . name } :${ typeof family . provider === 'string' ? family . provider : family . provider . name ! } ` ;
148+ let resolvedFamily = resolvedFamiliesMap . get ( key ) ;
149+ if ( ! resolvedFamily ) {
150+ if (
151+ Array . from ( resolvedFamiliesMap . keys ( ) ) . find ( ( k ) => k . startsWith ( `${ family . cssVariable } :` ) )
152+ ) {
153+ logger . warn (
154+ 'assets' ,
155+ `Several font families have been registered for the ${ bold ( family . cssVariable ) } cssVariable but they do not share the same name and provider.` ,
156+ ) ;
157+ logger . warn (
158+ 'assets' ,
159+ 'These families will not be merged together. The last occurrence will override previous families for this cssVariable. Review your Astro configuration.' ,
160+ ) ;
161+ }
162+ resolvedFamily = {
163+ family,
164+ fonts : [ ] ,
165+ fallbacks : family . fallbacks ?? defaults . fallbacks ?? [ ] ,
166+ collectedFonts : [ ] ,
167+ preloadData : [ ] ,
168+ } ;
169+ resolvedFamiliesMap . set ( key , resolvedFamily ) ;
170+ }
135171
136172 /**
137173 * Allows collecting and transforming original URLs from providers, so the Vite
@@ -144,26 +180,26 @@ export async function orchestrate({
144180 fontFileDataMap . set ( hash , { url, init } ) ;
145181 } ,
146182 savePreload : ( preload ) => {
147- preloadData . push ( preload ) ;
183+ resolvedFamily . preloadData . push ( preload ) ;
148184 } ,
149185 saveFontData : ( collected ) => {
150186 if (
151- fallbacks &&
152- fallbacks . length > 0 &&
187+ resolvedFamily . fallbacks &&
188+ resolvedFamily . fallbacks . length > 0 &&
153189 // If the same data has already been sent for this family, we don't want to have
154190 // duplicated fallbacks. Such scenario can occur with unicode ranges.
155- ! collectedFonts . some ( ( f ) => JSON . stringify ( f . data ) === JSON . stringify ( collected . data ) )
191+ ! resolvedFamily . collectedFonts . some (
192+ ( f ) => JSON . stringify ( f . data ) === JSON . stringify ( collected . data ) ,
193+ )
156194 ) {
157195 // If a family has fallbacks, we store the first url we get that may
158196 // be used for the fallback generation.
159- collectedFonts . push ( collected ) ;
197+ resolvedFamily . collectedFonts . push ( collected ) ;
160198 }
161199 } ,
162200 cssVariable : family . cssVariable ,
163201 } ) ;
164202
165- let fonts : Array < unifont . FontFaceData > ;
166-
167203 if ( family . provider === LOCAL_PROVIDER_NAME ) {
168204 const result = resolveLocalFont ( {
169205 family,
@@ -172,7 +208,7 @@ export async function orchestrate({
172208 fontFileReader,
173209 } ) ;
174210 // URLs are already proxied at this point so no further processing is required
175- fonts = result . fonts ;
211+ resolvedFamily . fonts . push ( ... result . fonts ) ;
176212 } else {
177213 const result = await resolveFont (
178214 family . name ,
@@ -194,16 +230,35 @@ export async function orchestrate({
194230 `No data found for font family ${ bold ( family . name ) } . Review your configuration` ,
195231 ) ;
196232 const availableFamilies = await listFonts ( [ family . provider . name ! ] ) ;
197- if ( availableFamilies && ! availableFamilies . includes ( family . name ) ) {
233+ if (
234+ availableFamilies &&
235+ availableFamilies . length > 0 &&
236+ ! availableFamilies . includes ( family . name )
237+ ) {
198238 logger . warn (
199239 'assets' ,
200240 `${ bold ( family . name ) } font family cannot be retrieved by the provider. Did you mean ${ bold ( stringMatcher . getClosestMatch ( family . name , availableFamilies ) ) } ?` ,
201241 ) ;
202242 }
203243 }
204244 // The data returned by the remote provider contains original URLs. We proxy them.
205- fonts = normalizeRemoteFontFaces ( { fonts : result . fonts , urlProxy, fontTypeExtractor } ) ;
245+ resolvedFamily . fonts = dedupeFontFaces (
246+ resolvedFamily . fonts ,
247+ normalizeRemoteFontFaces ( { fonts : result . fonts , urlProxy, fontTypeExtractor } ) ,
248+ ) ;
206249 }
250+ }
251+
252+ // We know about all the families, let's generate css, fallbacks and more
253+ for ( const {
254+ family,
255+ fonts,
256+ fallbacks,
257+ collectedFonts,
258+ preloadData,
259+ } of resolvedFamiliesMap . values ( ) ) {
260+ const consumableMapValue : Array < FontData > = [ ] ;
261+ let css = '' ;
207262
208263 for ( const data of fonts ) {
209264 css += cssRenderer . generateFontFace (
0 commit comments