Skip to content

Commit

Permalink
fix: inject relative font urls in css
Browse files Browse the repository at this point in the history
resolves #147
  • Loading branch information
danielroe committed Sep 11, 2024
1 parent 572f3ca commit b7646e9
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 8 deletions.
16 changes: 15 additions & 1 deletion src/css/render.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { hasProtocol } from 'ufo'
import { extname } from 'pathe'
import { extname, relative } from 'pathe'
import { getMetricsForFamily, generateFontFace as generateFallbackFontFace, readMetrics } from 'fontaine'
import type { FontSource, NormalizedFontFaceData, RemoteFontSource } from '../types'

Expand Down Expand Up @@ -79,3 +79,17 @@ function renderFontSrc(sources: Exclude<FontSource, string>[]) {
return `local("${src.name}")`
}).join(', ')
}

export function relativiseFontSources(font: NormalizedFontFaceData, relativeTo: string) {
return {
...font,
src: font.src.map((source) => {
if ('name' in source) return source
if (!source.url.startsWith('/')) return source
return {
...source,
url: relative(relativeTo, source.url),
}
}),
} satisfies NormalizedFontFaceData
}
10 changes: 6 additions & 4 deletions src/plugins/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import MagicString from 'magic-string'
import { transform } from 'esbuild'
import type { TransformOptions } from 'esbuild'
import type { ESBuildOptions } from 'vite'
import { dirname } from 'pathe'
import { withLeadingSlash } from 'ufo'

import type { Awaitable, NormalizedFontFaceData, RemoteFontSource } from '../types'
import type { GenericCSSFamily } from '../css/parse'
import { extractEndOfFirstChild, extractFontFamilies, extractGeneric } from '../css/parse'
import { generateFontFace, generateFontFallbacks } from '../css/render'
import { generateFontFace, generateFontFallbacks, relativiseFontSources } from '../css/render'

export interface FontFaceResolution {
fonts?: NormalizedFontFaceData[]
Expand All @@ -29,7 +31,7 @@ const SKIP_RE = /\/node_modules\/vite-plugin-vue-inspector\//
export const FontFamilyInjectionPlugin = (options: FontFamilyInjectionPluginOptions) => createUnplugin(() => {
let postcssOptions: Parameters<typeof transform>[1] | undefined

async function transformCSS(code: string, id: string) {
async function transformCSS(code: string, id: string, opts: { relative?: boolean } = {}) {
const s = new MagicString(code)

const injectedDeclarations = new Set<string>()
Expand Down Expand Up @@ -62,7 +64,7 @@ export const FontFamilyInjectionPlugin = (options: FontFamilyInjectionPluginOpti

for (const font of result.fonts) {
const fallbackDeclarations = await generateFontFallbacks(fontFamily, font, fallbackMap)
const declarations = [generateFontFace(fontFamily, font), ...fallbackDeclarations]
const declarations = [generateFontFace(fontFamily, opts.relative ? relativiseFontSources(font, withLeadingSlash(dirname(id))) : font), ...fallbackDeclarations]

for (let declaration of declarations) {
if (!injectedDeclarations.has(declaration)) {
Expand Down Expand Up @@ -181,7 +183,7 @@ export const FontFamilyInjectionPlugin = (options: FontFamilyInjectionPluginOpti
for (const key in bundle) {
const chunk = bundle[key]!
if (chunk?.type === 'asset' && isCSS(chunk.fileName)) {
const s = await transformCSS(chunk.source.toString(), key)
const s = await transformCSS(chunk.source.toString(), key, { relative: true })
if (s.hasChanged()) {
chunk.source = s.toString()
}
Expand Down
50 changes: 50 additions & 0 deletions test/base-url.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { fileURLToPath } from 'node:url'
import { describe, it, expect } from 'vitest'
import { setup, $fetch } from '@nuxt/test-utils'

import { extractPreloadLinks } from './utils'

await setup({
rootDir: fileURLToPath(new URL('../playground', import.meta.url)),
env: {
NUXT_APP_BASE_URL: '/foo',
},
})

describe('custom base URL', async () => {
const providers = ['adobe', 'bunny', 'fontshare', 'fontsource', 'google']

it('respects custom baseURL in preload links', async () => {
for (const provider of providers) {
const html = await $fetch<string>(`/foo/providers/${provider}`)
const links = extractPreloadLinks(html)
expect(links.every(link => link?.includes('/foo'))).toBeTruthy()
}
})

// TODO: this test fails in testing but succeeds when run locally
it.todo('respects custom baseURL in inline styles', async () => {
function startsWithBase(url: string) {
return url.startsWith('/foo')
}
for (const provider of providers) {
const html = await $fetch<string>(`/foo/providers/${provider}`)
for (const block of html.matchAll(/<style>([\s\S]+)<\/style>/g)) {
const fontUrls = [...block[1]!.matchAll(/url\(([^)]+)\)/g)].map(url => url[1])
for (const url of fontUrls) {
expect.soft(url).toSatisfy(startsWithBase)
}
}
}
})

it('renders font URLs relatively in CSS', async () => {
for (const provider of providers) {
const html = await $fetch<string>(`/foo/providers/${provider}`)
const cssLink = html.match(/<link rel="stylesheet" href="([^"]+)">/)![1]!
const css = await $fetch<string>(cssLink)
const fontUrls = css.match(/url\(([^)]+)\)/g)
expect(fontUrls!.every(url => url?.includes('../_fonts'))).toBeTruthy()
}
})
})
2 changes: 1 addition & 1 deletion test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ describe('features', () => {
const css = await $fetch<string>(cssFile)
const barlow = extractFontFaces('Barlow', css)
expect(barlow.length).toMatchInlineSnapshot(`8`)
expect(barlow[0]).toMatchInlineSnapshot(`"@font-face{font-family:Barlow;src:local("Barlow Regular Italic"),local("Barlow Italic"),url(/_fonts/file.woff2) format(woff2);font-display:swap;unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-weight:400;font-style:italic}"`)
expect(barlow[0]).toMatchInlineSnapshot(`"@font-face{font-family:Barlow;src:local("Barlow Regular Italic"),local("Barlow Italic"),url(../_fonts/file.woff2) format(woff2);font-display:swap;unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-weight:400;font-style:italic}"`)
})

it('adds preload links to the HTML with locally scoped rules', async () => {
Expand Down
4 changes: 2 additions & 2 deletions test/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export function extractFontFaces(fontFamily: string, html: string) {
const matches = html.matchAll(new RegExp(`@font-face\\s*{[^}]*font-family:\\s*(?<quote>['"])?${fontFamily}\\k<quote>[^}]+}`, 'g'))
return Array.from(matches, match => match[0]
.replace(/(?<=['"(])(https?:\/\/[^/]+|\/_fonts)\/[^")]+(\.[^".)]+)(?=['")])/g, '$1/file$2')
.replace(/(?<=['"(])(https?:\/\/[^/]+|\/_fonts)\/[^".)]+(?=['")])/g, '$1/file'),
.replace(/(?<=['"(])(https?:\/\/[^/]+|(?:..)?\/_fonts)\/[^")]+(\.[^".)]+)(?=['")])/g, '$1/file$2')
.replace(/(?<=['"(])(https?:\/\/[^/]+|(?:..)?\/_fonts)\/[^".)]+(?=['")])/g, '$1/file'),
)
}

Expand Down

0 comments on commit b7646e9

Please sign in to comment.