Skip to content

Commit

Permalink
feat: support configure injecting fonts globally
Browse files Browse the repository at this point in the history
  • Loading branch information
danielroe committed Feb 20, 2024
1 parent 18947c4 commit 26fa0b3
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 47 deletions.
1 change: 1 addition & 0 deletions playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export default defineNuxtConfig({
families: [
{ name: 'Kode Mono', provider: 'none' },
{ name: 'MyCustom', src: '/font.woff2' },
{ name: 'CustomGlobal', global: true, src: '/font-global.woff2' },
]
},
})
5 changes: 5 additions & 0 deletions playground/pages/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template>
<div>
Index
</div>
</template>
91 changes: 56 additions & 35 deletions src/module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { addBuildPlugin, defineNuxtModule, resolveAlias, resolvePath, useLogger } from '@nuxt/kit'
import { addBuildPlugin, addTemplate, defineNuxtModule, resolveAlias, resolvePath, useLogger } from '@nuxt/kit'

import google from './providers/google'
import local from './providers/local'

import { FontFamilyInjectionPlugin } from './transform'
import type { FontProvider, ModuleOptions, ResolveFontFacesOptions } from './types'
import { FontFamilyInjectionPlugin, generateFontFaces } from './plugins/transform'
import type { FontFaceData, FontFamilyManualOverride, FontFamilyProviderOverride, FontProvider, ModuleOptions, ResolveFontFacesOptions } from './types'

export type { ModuleOptions } from './types'

Expand Down Expand Up @@ -61,49 +61,70 @@ export default defineNuxtModule<ModuleOptions>({
await Promise.all(setups)
})

addBuildPlugin(FontFamilyInjectionPlugin({
async resolveFontFace (fontFamily) {
const override = options.families?.find(f => f.name === fontFamily)

if (!override) {
return resolveFontFace(providers, fontFamily, defaultValues as ResolveFontFacesOptions)
async function resolveFontFaceWithOverride (fontFamily: string, override: FontFamilyManualOverride | FontFamilyProviderOverride): Promise<FontFaceData | FontFaceData[] | undefined> {
if ('src' in override) {
return {
src: override.src,
display: override.display,
weight: override.weight,
style: override.style,
}
}

// Respect fonts that should not be resolved through `@nuxt/fonts`
if (override.provider === 'none') { return }

// Manual override
if ('src' in override) {
return {
src: override.src,
display: override.display,
weight: override.weight,
style: override.style,
// Respect custom weights, styles and subsets options
const defaults = {
weights: override.weights || defaultValues.weights,
styles: override.styles || defaultValues.styles,
subsets: override.subsets || defaultValues.subsets
}

// Handle explicit provider
if (override.provider) {
if (override.provider in providers) {
const result = await providers[override.provider]!.resolveFontFaces!(fontFamily, override as ResolveFontFacesOptions)
if (!result) {
return logger.warn(`Could not produce font face declaration from \`${override.provider}\` for font family \`${fontFamily}\`.`)
}
return result?.fonts
}

// Respect fonts that should not be resolved through `@nuxt/fonts`
if (override.provider === 'none') { return }
// If not registered, log and fall back to default providers
logger.warn(`Unknown provider \`${override.provider}\` for font family \`${fontFamily}\`. Falling back to default providers.`)
}

return resolveFontFace(providers, fontFamily, defaults)
}

// Respect custom weights, styles and subsets options
const defaults = {
weights: override.weights || defaultValues.weights,
styles: override.styles || defaultValues.styles,
subsets: override.subsets || defaultValues.subsets
nuxt.options.css.push('#build/nuxt-fonts-global.css')
addTemplate({
filename: 'nuxt-fonts-global.css',
write: true, // Seemingly necessary to allow vite to process file 🤔
async getContents () {
let css = ''
for (const family of options.families || []) {
if (!family.global) continue
const result = await resolveFontFaceWithOverride(family.name, family)
if (result) { css += generateFontFaces(family.name, result).join('\n') + '\n' }
}
return css
}
})

// Handle explicit provider
if (override.provider) {
if (override.provider in providers) {
const result = await providers[override.provider]!.resolveFontFaces!(fontFamily, override as ResolveFontFacesOptions)
if (!result) {
return logger.warn(`Could not produce font face declaration from \`${override.provider}\` for font family \`${fontFamily}\`.`)
}
return result?.fonts
}
addBuildPlugin(FontFamilyInjectionPlugin({
async resolveFontFace (fontFamily) {
const override = options.families?.find(f => f.name === fontFamily)

// If not registered, log and fall back to default providers
logger.warn(`Unknown provider \`${override.provider}\` for font family \`${fontFamily}\`. Falling back to default providers.`)
if (!override) {
return resolveFontFace(providers, fontFamily, defaultValues as ResolveFontFacesOptions)
}

return resolveFontFace(providers, fontFamily, defaults)
// This CSS will be injected in a separate location
if (override.global) { return }

return resolveFontFaceWithOverride(fontFamily, override)
}
}))
}
Expand Down
7 changes: 3 additions & 4 deletions src/transform.ts → src/plugins/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ import MagicString from 'magic-string'
import { extname } from 'pathe'
import { hasProtocol } from 'ufo'

import type { FontFaceData, FontSource } from './types'

type Awaitable<T> = T | Promise<T>
import type { Awaitable, FontFaceData, FontSource } from '../types'

interface FontFamilyInjectionPluginOptions {
resolveFontFace: (fontFamily: string) => Awaitable<FontFaceData | FontFaceData[] | undefined>
}

// TODO: support shared chunks of CSS
export const FontFamilyInjectionPlugin = (options: FontFamilyInjectionPluginOptions) => createUnplugin(() => {
return {
name: 'nuxt:fonts:font-family-injection',
Expand Down Expand Up @@ -97,7 +96,7 @@ const genericCSSFamilies = new Set([
'unset',
])

function generateFontFaces (family: string, source: FontFaceData | FontFaceData[]) {
export function generateFontFaces (family: string, source: FontFaceData | FontFaceData[]) {
const sources = Array.isArray(source) ? source : [source]
const declarations: string[] = []
for (const font of sources) {
Expand Down
20 changes: 13 additions & 7 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Nuxt } from '@nuxt/schema'

type Awaitable<T> = T | Promise<T>
export type Awaitable<T> = T | Promise<T>

export interface RemoteFontSource {
url: string
Expand Down Expand Up @@ -73,8 +73,12 @@ export type FontProviderName = (string & {}) | 'google' | 'local' | 'none'


export interface FontFamilyOverrides {
/** The font family to apply this override to. */
name: string
/** Inject `@font-face` regardless of usage in project. */
global?: boolean

// TODO:
// as?: string
// fallbacks?: Array<string>
}
Expand All @@ -85,7 +89,8 @@ export interface FontFamilyProviderOverride extends FontFamilyOverrides {
styles?: Array<'normal' | 'italic' | 'oblique'>
subsets?: string[]

fallbacks?: string[]
// TODO:
// fallbacks?: string[]
}

export interface FontFamilyManualOverride extends FontFamilyOverrides, FontFaceData {}
Expand Down Expand Up @@ -115,16 +120,17 @@ export interface ModuleOptions {
[key: string]: FontProvider | string | false | undefined
}
// TODO: Provider options
// google?: {}
// google?: {
// // TODO: allow customising download behaviour with nuxt/assets
// download?: string
// }
// local?: {}
/**
* An ordered list of providers to check when resolving font families.
*
* Default behaviour is to check all user providers in the order they were defined, and then all built-in providers.
*/
priority?: string[]
// priority?: string[]
// TODO: support default provider
provider?: FontProviderName
// TODO: allow customising download behaviour with nuxt/assets
// download?: string
// provider?: FontProviderName
}
11 changes: 10 additions & 1 deletion test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ describe('features', () => {
`)
})

it('should allow globally registered font', async () => {
const html = await $fetch('/')
expect(extractFontFaces('CustomGlobal', html)).toMatchInlineSnapshot(`
[
"@font-face{font-display:swap;font-family:CustomGlobal;src:url(/font-global.woff2) format(woff2)}",
]
`)
})

it('supports external files and scss syntax', async () => {
const html = await $fetch('/preprocessors')
expect(extractFontFaces('Anta', html)).toMatchInlineSnapshot(`
Expand Down Expand Up @@ -101,7 +110,7 @@ describe('features', () => {
})

function extractFontFaces (fontFamily: string, html: string) {
const matches = html.matchAll(new RegExp(`@font-face {\\s*font-family: '${fontFamily}'[^}]+}`, 'g'))
const matches = html.matchAll(new RegExp(`@font-face\\s*{[^}]*font-family:\\s*['"]?${fontFamily}['"]?[^}]+}`, 'g'))
return Array.from(matches, (match) => match[0]
.replace(/"(https?:\/\/[^/]+)\/[^"]+(\.[^".]+)"/g, '"$1/file$2"')
.replace(/"(https?:\/\/[^/]+)\/[^".]+"/g, '"$1/file"')
Expand Down

0 comments on commit 26fa0b3

Please sign in to comment.