Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fontsource provider #78

Merged
merged 7 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Plug-and-play custom web font optimization and configuration for Nuxt apps.
## Features

- ✨ zero-configuration required
- 🔡 built-in providers (`google`, `bunny`, `fontshare`, `adobe`, `local` - more welcome!)
- 🔡 built-in providers (`google`, `bunny`, `fontshare`, `fontsource`, `adobe`, `local` - more welcome!)
- 💪 custom providers for full control
- ⏬ local download support (until `nuxt/assets` lands)
- ⚡️ automatic font metric optimisation powered by [**fontaine**](https://github.com/unjs/fontaine) and [**capsize**](https://github.com/seek-oss/capsize)
Expand Down Expand Up @@ -187,6 +187,10 @@ Then, when you use a `font-family` in your CSS, we check to see whether it match

You should read [their terms in full](https://www.fontshare.com/licenses/itf-ffl) before using a font through `fontshare`.

### `fontsource`

[Fontsource](https://fontsource.org/docs/getting-started/introduction) is a collection of open-source fonts that are designed for self-hosting in web applications.

### `adobe`

[Adobe Fonts](https://fonts.adobe.com/) is a font service for both personal and commercial use included with Creative Cloud subscriptions.
Expand Down
3 changes: 2 additions & 1 deletion playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export default defineNuxtConfig({
{ name: 'CustomGlobal', global: true, src: '/font-global.woff2' },
{ name: 'Oswald', fallbacks: ['Times New Roman'] },
{ name: 'Aleo', provider: 'adobe'},
{ name: 'Barlow Semi Condensed', provider: 'adobe' }
{ name: 'Barlow Semi Condensed', provider: 'adobe' },
{ name: 'Roboto Mono', provider: 'fontsource' },
],
adobe: {
id: ['sij5ufr', 'grx7wdj'],
Expand Down
11 changes: 11 additions & 0 deletions playground/pages/providers/fontsource.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<template>
<div>
fontsource
</div>
</template>

<style scoped>
div {
font-family: 'Roboto Mono', sans-serif;
}
</style>
2 changes: 2 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import google from './providers/google'
import bunny from './providers/bunny'
import fontshare from './providers/fontshare'
import adobe from './providers/adobe'
import fontsource from './providers/fontsource'

import { FontFamilyInjectionPlugin, type FontFaceResolution } from './plugins/transform'
import { generateFontFace } from './css/render'
Expand Down Expand Up @@ -95,6 +96,7 @@ export default defineNuxtModule<ModuleOptions>({
google,
bunny,
fontshare,
fontsource,
},
},
async setup (options, nuxt) {
Expand Down
127 changes: 127 additions & 0 deletions src/providers/fontsource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { $fetch } from 'ofetch'
import { hash } from 'ohash'

import type { FontProvider, NormalizedFontFaceData, ResolveFontFacesOptions } from '../types'
import { addLocalFallbacks } from '../css/parse'
import { cachedData } from '../cache'
import { logger } from '../logger'

export default {
async setup () {
await initialiseFontMeta()
},
async resolveFontFaces (fontFamily, defaults) {
if (!isFontsourceFont(fontFamily)) { return }

return {
fonts: await cachedData(`fontsource:${fontFamily}-${hash(defaults)}-data.json`, () => getFontDetails(fontFamily, defaults), {
onError (err) {
logger.error(`Could not fetch metadata for \`${fontFamily}\` from \`fontsource\`.`, err)
return []
}
})
}
},
} satisfies FontProvider

/** internal */

const fontAPI = $fetch.create({
baseURL: 'https://api.fontsource.org/v1'
})

export interface FontsourceFontMeta {
[key: string]: {
id: string
family: string
subsets: string[]
weights: number[]
styles: string[]
defSubset: string
variable: boolean
lastModified: string
category: string
version: string
type: string
}
}

interface FontsourceFontFile {
url: {
woff2?: string
woff?: string
ttf?: string
}
}

interface FontsourceFontVariant {
[key: string]: {
[key: string]: {
[key: string]: FontsourceFontFile
}
}
}

interface FontsourceFontDetail {
id: string
family: string
subsets: string[]
weights: number[]
styles: string[]
unicodeRange: Record<string, string>
defSubset: string
variable: boolean
lastModified: string
category: string
version: string
type: string
variants: FontsourceFontVariant
}

let fonts: FontsourceFontMeta
const familyMap = new Map<string, string>()

async function initialiseFontMeta () {
fonts = await cachedData('fontsource:meta.json', () => fontAPI<FontsourceFontMeta[]>('/fonts', { responseType: 'json' }), {
onError () {
logger.error('Could not download `fontsource` font metadata. `@nuxt/fonts` will not be able to inject `@font-face` rules for fontsource.')
return {}
}
})
for (const id in fonts) {
familyMap.set(fonts[id]!.family!, id)
}
}

function isFontsourceFont (family: string) {
return familyMap.has(family)
}


async function getFontDetails (family: string, variants: ResolveFontFacesOptions) {
const id = familyMap.get(family) as keyof typeof fonts
const font = fonts[id]!
const weights = variants.weights.filter(weight => font.weights.includes(Number(weight)))
const styles = variants.styles.filter(style => font.styles.includes(style))
if (weights.length === 0 || styles.length === 0) return []

const fontDetail = await fontAPI<FontsourceFontDetail>(`/fonts/${font.id}`, { responseType: 'json' })
const fontFaceData: NormalizedFontFaceData[] = []

// TODO: support subsets apart from default
const defaultSubset = fontDetail.defSubset

for (const weight of weights) {
for (const style of styles) {
const variantUrl = fontDetail.variants[weight]![style]![defaultSubset]!.url
fontFaceData.push({
style,
weight,
src: Object.entries(variantUrl).map(([format, url]) => ({ url, format })),
unicodeRange: fontDetail.unicodeRange[defaultSubset]?.split(',')
})
}
}

return addLocalFallbacks(family, fontFaceData)
}
10 changes: 10 additions & 0 deletions test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ describe('providers', async () => {
`)
})

it('generates inlined font face rules for `fontsource` provider', async () => {
const html = await $fetch('/providers/fontsource')
expect(extractFontFaces('Roboto Mono', html)).toMatchInlineSnapshot(`
[
"@font-face{font-family:Roboto Mono;src:local("Roboto Mono"),url(/_fonts/file.woff2) format(woff2),url(/_fonts/file.woff) format(woff),url(/_fonts/file.ttf) format(ttf);font-display:swap;unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-weight:400;font-style:italic}",
"@font-face{font-family:Roboto Mono;src:local("Roboto Mono"),url(/_fonts/file.woff2) format(woff2),url(/_fonts/file.woff) format(woff),url(/_fonts/file.ttf) format(ttf);font-display:swap;unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-weight:400;font-style:normal}",
]
`)
})

it('generates inlined font face rules for `google` provider', async () => {
const html = await $fetch('/providers/google')
const poppins = extractFontFaces('Poppins', html)
Expand Down