Skip to content

Commit 36509fe

Browse files
jack-old-archivezyyvautofix-ci[bot]
authored
feat(preset-web-fonts): add onDownload callback to createLocalFontProcessor (#5199)
Co-authored-by: Chris <hizyyv@gmail.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 42c8e78 commit 36509fe

2 files changed

Lines changed: 80 additions & 10 deletions

File tree

docs/presets/web-fonts.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,57 @@ This will download the fonts assets to `public/assets/fonts` and serve them from
253253
This feature is Node.js specific and will not work in the browser.
254254

255255
:::
256+
257+
## Emit Fonts to Build Output
258+
259+
In CI environments or during the first build, fonts downloaded to the `public` directory might not be copied to `dist` before the build finishes. To ensure fonts are always included in the production build, use the `onDownload` callback with a custom Vite plugin.
260+
261+
**vite.config.ts**
262+
263+
```ts
264+
import { createLocalFontProcessor } from '@unocss/preset-web-fonts/local'
265+
import { defineConfig } from 'vite'
266+
267+
const emittedFonts = new Map()
268+
269+
// 1. Create the processor with onDownload hook
270+
export const fontProcessor = createLocalFontProcessor({
271+
onDownload(filename, buf) {
272+
emittedFonts.set(filename, buf)
273+
}
274+
})
275+
276+
export default defineConfig({
277+
plugins: [
278+
UnoCSS(),
279+
// 2. Emit the collected fonts as assets during build
280+
{
281+
name: 'unocss:font-emit',
282+
apply: 'build',
283+
generateBundle() {
284+
for (const [filename, source] of emittedFonts) {
285+
this.emitFile({ type: 'asset', fileName: `assets/fonts/${filename}`, source })
286+
}
287+
emittedFonts.clear()
288+
}
289+
},
290+
],
291+
})
292+
```
293+
294+
**uno.config.ts**
295+
296+
```ts
297+
import presetWebFonts from '@unocss/preset-web-fonts'
298+
import { fontProcessor } from './vite.config'
299+
300+
export default defineConfig({
301+
presets: [
302+
presetWebFonts({
303+
provider: 'google',
304+
fonts: { sans: 'Roboto' },
305+
processors: [fontProcessor],
306+
}),
307+
],
308+
})
309+
```

packages-presets/preset-web-fonts/src/local.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,9 @@ import fsp from 'node:fs/promises'
66
import { join, resolve } from 'node:path'
77
import process from 'node:process'
88
import { fetch } from 'ofetch'
9-
import { replaceAsync } from '#integration/utils'
109

1110
const fontUrlRegex = /[-\w@:%+.~#?&/=]+\.(?:woff2?|eot|ttf|otf|svg)/gi
12-
// eslint-disable-next-line regexp/no-unused-capturing-group
13-
const urlProtocolRegex = /^[\s\w\0+.-]{2,}:([/\\]{1,2})/
11+
const urlProtocolRegex = /^[\s\w\0+.-]{2,}:[/\\]{1,2}/
1412

1513
export interface LocalFontProcessorOptions {
1614
/**
@@ -45,6 +43,13 @@ export interface LocalFontProcessorOptions {
4543
* Custom fetch function to provide the font data.
4644
*/
4745
fetch?: typeof fetch
46+
47+
/**
48+
* Callback invoked when a font file is downloaded during build.
49+
* Receives the filename and file buffer, useful for emitting fonts
50+
* directly to the build output instead of relying on public dir copy.
51+
*/
52+
onDownload?: (filename: string, buf: Buffer) => void | Promise<void>
4853
}
4954

5055
export function createLocalFontProcessor(options?: LocalFontProcessorOptions): WebFontProcessor {
@@ -58,8 +63,11 @@ export function createLocalFontProcessor(options?: LocalFontProcessorOptions): W
5863
const fetcher = options?.fetch ?? fetch
5964
const response = await fetcher(url)
6065
.then(r => r.arrayBuffer())
66+
const buf = Buffer.from(response)
67+
const filename = assetPath.split(/[\\/]/).pop()!
68+
await options?.onDownload?.(filename, buf)
6169
await fsp.mkdir(fontAssetsDir, { recursive: true })
62-
await fsp.writeFile(assetPath, Buffer.from(response))
70+
await fsp.writeFile(assetPath, buf)
6371
}
6472

6573
const cache = new Map<string, Promise<void>>()
@@ -75,9 +83,9 @@ export function createLocalFontProcessor(options?: LocalFontProcessorOptions): W
7583
const hash = getHash(JSON.stringify(fonts))
7684
const cachePath = join(cacheDir, `${hash}.css`)
7785

78-
if (fs.existsSync(cachePath)) {
86+
if (fs.existsSync(cachePath))
7987
return fsp.readFile(cachePath, 'utf-8')
80-
}
88+
8189
const css = await getCSSDefault(fonts, providers)
8290

8391
await fsp.mkdir(cacheDir, { recursive: true })
@@ -86,12 +94,17 @@ export function createLocalFontProcessor(options?: LocalFontProcessorOptions): W
8694
return css
8795
},
8896
async transformCSS(css) {
89-
return await replaceAsync(css, fontUrlRegex, async (url) => {
97+
// Find all font URLs in the CSS
98+
const matches = css.match(fontUrlRegex) || []
99+
const replacements = new Map<string, string>()
100+
101+
// Process each unique URL found in the CSS
102+
await Promise.all(Array.from(new Set(matches)).map(async (url) => {
90103
const hash = getHash(url)
91104
const ext = url.split('.').pop()
92105

93106
let name = ''
94-
const match1 = url.match(/\/s\/([^/]+)\//) // Google Fonts
107+
const match1 = url.match(/\/s\/([^/]+)\//)
95108
if (match1)
96109
name = match1[1].replace(/\W/g, ' ').trim().replace(/\s+/, '-').toLowerCase()
97110

@@ -103,8 +116,11 @@ export function createLocalFontProcessor(options?: LocalFontProcessorOptions): W
103116
await downloadFont(_url, assetPath)
104117
}
105118

106-
return `${fontServeBaseUrl}/${filename}`
107-
})
119+
replacements.set(url, `${fontServeBaseUrl}/${filename}`)
120+
}))
121+
122+
// Replace all occurrences using the populated map
123+
return css.replace(fontUrlRegex, match => replacements.get(match) || match)
108124
},
109125
}
110126
}

0 commit comments

Comments
 (0)