(
if (injectExtra.length > 0)
injects.push(...injectExtra)
+ await configureWebFontPreset(config, defaults)
+
updateConfig({
vite: {
plugins: [AstroVitePlugin({
diff --git a/packages/astro/src/web-fonts.ts b/packages/astro/src/web-fonts.ts
new file mode 100644
index 0000000000..58223a2fc1
--- /dev/null
+++ b/packages/astro/src/web-fonts.ts
@@ -0,0 +1,21 @@
+import { resolve } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import type { Preset, UserConfigDefaults } from '@unocss/core'
+import type { AstroConfig } from 'astro'
+
+export async function configureWebFontPreset(config: AstroConfig, options?: UserConfigDefaults) {
+ const webFontPreset = options ? lookupPreset(options, '@unocss/preset-web-fonts') : undefined
+ if (webFontPreset && !!webFontPreset.options?.downloadLocally) {
+ const { defaultFontFolder } = await import('@unocss/preset-web-fonts/local-font')
+ const downloadDir = resolve(fileURLToPath(config.publicDir), defaultFontFolder)
+ webFontPreset.options.downloadLocally = {
+ downloadDir,
+ downloadBasePath: config.base,
+ }
+ }
+}
+
+function lookupPreset>(options: UserConfigDefaults, presetName: P['name']) {
+ const preset: P | undefined = (options.presets || []).flat().find(p => p.name === presetName) as any
+ return preset
+}
diff --git a/packages/nuxt/build.config.ts b/packages/nuxt/build.config.ts
index 920b5f6f29..29033bb876 100644
--- a/packages/nuxt/build.config.ts
+++ b/packages/nuxt/build.config.ts
@@ -8,5 +8,6 @@ export default defineBuildConfig({
declaration: true,
externals: [
'@nuxt/schema',
+ 'pathe',
],
})
diff --git a/packages/nuxt/src/index.ts b/packages/nuxt/src/index.ts
index 7ca2c08a8a..39652fb1c2 100644
--- a/packages/nuxt/src/index.ts
+++ b/packages/nuxt/src/index.ts
@@ -10,6 +10,7 @@ import { loadConfig } from '@unocss/config'
import type { UserConfig } from '@unocss/core'
import { resolveOptions } from './options'
import type { UnocssNuxtOptions } from './types'
+import { configureWebFontPreset } from './web-fonts'
export { UnocssNuxtOptions }
@@ -38,6 +39,9 @@ export default defineNuxtModule({
// preset shortcuts
resolveOptions(options)
+ // configure local webfonts preset
+ await configureWebFontPreset(nuxt, options)
+
options.mode ??= 'global'
const InjectModes: VitePluginConfig['mode'][] = ['global', 'dist-chunk']
diff --git a/packages/nuxt/src/web-fonts.ts b/packages/nuxt/src/web-fonts.ts
new file mode 100644
index 0000000000..48fab872eb
--- /dev/null
+++ b/packages/nuxt/src/web-fonts.ts
@@ -0,0 +1,23 @@
+import type { Preset } from '@unocss/core'
+import type { Nuxt } from '@nuxt/schema'
+import { dirname, relative, resolve } from 'pathe'
+import type { UnocssNuxtOptions } from './types'
+
+export async function configureWebFontPreset(nuxt: Nuxt, options: UnocssNuxtOptions) {
+ const webFontPreset = lookupPreset(options, '@unocss/preset-web-fonts')
+ if (webFontPreset && !!webFontPreset.options?.downloadLocally) {
+ const { defaultFontFolder, defaultFontCssFilename } = await import('@unocss/preset-web-fonts/local-font')
+ const downloadDir = resolve(nuxt.options.dir.public, defaultFontFolder)
+ webFontPreset.options.downloadLocally = {
+ downloadDir,
+ downloadBasePath: nuxt.options.app.baseURL,
+ }
+ nuxt.options.css ??= []
+ nuxt.options.css.push(`~/${relative(dirname(nuxt.options.dir.public), downloadDir)}/${defaultFontCssFilename}`.replaceAll('\\', '/'))
+ }
+}
+
+function lookupPreset>(options: UnocssNuxtOptions, presetName: P['name']) {
+ const preset: P | undefined = (options.presets || []).flat().find(p => p.name === presetName) as any
+ return preset
+}
diff --git a/packages/preset-web-fonts/build.config.ts b/packages/preset-web-fonts/build.config.ts
index a308702c04..027876400e 100644
--- a/packages/preset-web-fonts/build.config.ts
+++ b/packages/preset-web-fonts/build.config.ts
@@ -3,6 +3,8 @@ import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
entries: [
'src/index',
+ 'src/local-font',
+ 'src/remote-font',
],
clean: true,
declaration: true,
diff --git a/packages/preset-web-fonts/package.json b/packages/preset-web-fonts/package.json
index 7cfe805374..7660697846 100644
--- a/packages/preset-web-fonts/package.json
+++ b/packages/preset-web-fonts/package.json
@@ -27,11 +27,27 @@
".": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
+ },
+ "./local-font": {
+ "types": "./dist/local-font.d.mts",
+ "default": "./dist/local-font.mjs"
+ },
+ "./remote-font": {
+ "types": "./dist/remote-font.d.mts",
+ "default": "./dist/remote-font.mjs"
}
},
"main": "dist/index.mjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
+ "typesVersions": {
+ "*": {
+ "*": [
+ "./dist/*",
+ "./*"
+ ]
+ }
+ },
"files": [
"*.css",
"dist"
diff --git a/packages/preset-web-fonts/src/local-font.ts b/packages/preset-web-fonts/src/local-font.ts
new file mode 100644
index 0000000000..bd1997e063
--- /dev/null
+++ b/packages/preset-web-fonts/src/local-font.ts
@@ -0,0 +1,67 @@
+/**
+ * Inspired by:
+ * https://github.com/feat-agency/vite-plugin-webfont-dl/blob/master/src/downloader.ts
+ */
+import { resolveDownloadDir } from './util'
+
+const fontUrlRegex = /[-a-z0-9@:%_+.~#?&/=]+\.(?:woff2?|eot|ttf|otf|svg)/gi
+
+export const defaultFontFolder = 'unocss-fonts'
+export const defaultFontCssFilename = 'fonts.css'
+
+interface UseLocalFontOptions {
+ downloadDir: string
+ downloadBasePath: string
+}
+
+export { resolveDownloadDir }
+
+export async function useLocalFont(css: string, { downloadDir, downloadBasePath }: UseLocalFontOptions) {
+ const [{ mkdir, writeFile }, { resolve }] = await Promise.all([
+ import('node:fs/promises'),
+ import('node:path'),
+ ])
+ await mkdir(downloadDir, { recursive: true })
+
+ // Download the fonts locally and update the font.css file
+ for (const url of css.match(fontUrlRegex) || []) {
+ const name = url.split('/').pop()!
+ await saveFont(url, resolve(downloadDir, name))
+ css = css.replaceAll(url, `${downloadBasePath}${defaultFontFolder}/${name}`)
+ }
+
+ // Save the updated font.css file
+ const fontCssPath = resolve(downloadDir, defaultFontCssFilename)
+ await writeFile(fontCssPath, css)
+}
+
+async function fileExists(path: string) {
+ const { lstat } = await import('node:fs/promises')
+ return await lstat(path).then(({ isFile }) => isFile()).catch(() => false)
+}
+
+async function saveFont(url: string, path: string) {
+ if (await fileExists(path))
+ return
+
+ const [{ writeFile }, { $fetch }, { Buffer }] = await Promise.all([
+ import('node:fs/promises'),
+ import('ofetch'),
+ import('node:buffer'),
+ ])
+ const response = await $fetch(url, { headers: { responseType: 'arraybuffer' } }) as ArrayBuffer
+ const content = new Uint8Array(response)
+ await writeFile(path, Buffer.from(content))
+}
+
+export async function readFontCSS(downloadDir: string) {
+ const [{ resolve }, { readFile }] = await Promise.all([
+ import('node:path'),
+ import('node:fs/promises'),
+ ])
+ const fontCssPath = resolve(downloadDir, defaultFontCssFilename)
+ if (!await fileExists(fontCssPath))
+ return '/* [preset-web-font] This code will be replaced with the local CSS once it is downloaded */'
+
+ return await readFile(fontCssPath, { encoding: 'utf-8' })
+}
diff --git a/packages/preset-web-fonts/src/preset.ts b/packages/preset-web-fonts/src/preset.ts
index 2d6a1025f4..b85c31a976 100644
--- a/packages/preset-web-fonts/src/preset.ts
+++ b/packages/preset-web-fonts/src/preset.ts
@@ -5,6 +5,7 @@ import { BunnyFontsProvider } from './providers/bunny'
import { GoogleFontsProvider } from './providers/google'
import { FontshareProvider } from './providers/fontshare'
import { NoneProvider } from './providers/none'
+import { isNode } from './util'
import type { Provider, ResolvedWebFontMeta, WebFontMeta, WebFontsOptions, WebFontsProviders } from './types'
const builtinProviders = {
@@ -45,60 +46,37 @@ export function createWebFontPreset(fetcher: (url: string) => Promise) {
inlineImports = true,
themeKey = 'fontFamily',
customFetch = fetcher,
+ downloadLocally,
} = options
const fontObject = Object.fromEntries(
Object.entries(options.fonts || {})
.map(([name, meta]) => [name, toArray(meta).map(m => normalizedFontMeta(m, defaultProvider))]),
)
- const fonts = Object.values(fontObject).flatMap(i => i)
-
- const importCache: Record> = {}
-
- async function importUrl(url: string) {
- if (inlineImports) {
- if (!importCache[url]) {
- importCache[url] = customFetch(url).catch((e) => {
- console.error('Failed to fetch web fonts')
- console.error(e)
- // eslint-disable-next-line node/prefer-global/process
- if (typeof process !== 'undefined' && process.env.CI)
- throw e
- })
- }
- return await importCache[url]
- }
- else {
- return `@import url('${url}');`
- }
- }
-
- const enabledProviders = new Set(fonts.map(i => i.provider))
const preset: Preset = {
name: '@unocss/preset-web-fonts',
preflights: [
{
async getCSS() {
- const preflights: (string | undefined)[] = []
-
- for (const provider of enabledProviders) {
- const fontsForProvider = fonts.filter(i => i.provider.name === provider.name)
-
- if (provider.getImportUrl) {
- const url = provider.getImportUrl(fontsForProvider)
- if (url)
- preflights.push(await importUrl(url))
- }
-
- preflights.push(provider.getPreflight?.(fontsForProvider))
+ if (!isNode || !downloadLocally) {
+ const { getRemoteFontsCSS } = await import('./remote-font')
+ return getRemoteFontsCSS(fontObject, { inlineImports, customFetch })
}
- return preflights.filter(Boolean).join('\n')
+ const { readFontCSS, resolveDownloadDir } = await import('./local-font')
+ const resolvedDownloadDir = await resolveDownloadDir(
+ downloadLocally === true ? undefined : downloadLocally.downloadDir,
+ )
+ return readFontCSS(resolvedDownloadDir)
},
layer: inlineImports ? undefined : LAYER_IMPORTS,
},
],
+ options: {
+ downloadLocally,
+ fontObject,
+ },
}
if (extendTheme) {
diff --git a/packages/preset-web-fonts/src/remote-font.ts b/packages/preset-web-fonts/src/remote-font.ts
new file mode 100644
index 0000000000..634ddf5e2d
--- /dev/null
+++ b/packages/preset-web-fonts/src/remote-font.ts
@@ -0,0 +1,44 @@
+import type { ResolvedWebFontMeta, WebFontsOptions } from './types'
+
+type UseRemoteFontOptions = Required>
+
+export async function getRemoteFontsCSS(fontObject: { [k: string]: ResolvedWebFontMeta[] }, { inlineImports, customFetch }: UseRemoteFontOptions) {
+ const fonts = Object.values(fontObject).flatMap(i => i)
+
+ const importCache: Record> = {}
+
+ async function importUrl(url: string) {
+ if (inlineImports) {
+ if (!importCache[url]) {
+ importCache[url] = customFetch(url).catch((e) => {
+ console.error('Failed to fetch web fonts')
+ console.error(e)
+ // eslint-disable-next-line node/prefer-global/process
+ if (typeof process !== 'undefined' && process.env.CI)
+ throw e
+ })
+ }
+ return await importCache[url]
+ }
+ else {
+ return `@import url('${url}');`
+ }
+ }
+
+ const preflights: (string | undefined)[] = []
+ const enabledProviders = new Set(fonts.map(i => i.provider))
+
+ for (const provider of enabledProviders) {
+ const fontsForProvider = fonts.filter(i => i.provider.name === provider.name)
+
+ if (provider.getImportUrl) {
+ const url = provider.getImportUrl(fontsForProvider)
+ if (url)
+ preflights.push(await importUrl(url))
+ }
+
+ preflights.push(provider.getPreflight?.(fontsForProvider))
+ }
+
+ return preflights.filter(Boolean).join('\n')
+}
diff --git a/packages/preset-web-fonts/src/types.ts b/packages/preset-web-fonts/src/types.ts
index 1c8a34be29..18fa037eb7 100644
--- a/packages/preset-web-fonts/src/types.ts
+++ b/packages/preset-web-fonts/src/types.ts
@@ -53,6 +53,30 @@ export interface WebFontsOptions {
* @default undefined
*/
customFetch?: (url: string) => Promise
+
+ /**
+ * Download fonts locally.
+ *
+ * @default false
+ */
+ downloadLocally?: boolean | {
+ /**
+ * Where to download the fonts.
+ *
+ * This option will be overridden by integrations:
+ * - Vite, Astro and Nuxt will use Vite [publicDir](): `${publicDir}/unocss-fonts`
+ * - SvelteKit will use `static` folder: `static/unocss-fonts`
+ *
+ * @default 'process.cwd()/public/unocss-fonts'
+ */
+ downloadDir?: string
+ /**
+ *
+ *
+ * @default /
+ */
+ downloadBasePath?: string
+ }
}
export interface Provider {
diff --git a/packages/preset-web-fonts/src/util.ts b/packages/preset-web-fonts/src/util.ts
new file mode 100644
index 0000000000..54e4d94dd0
--- /dev/null
+++ b/packages/preset-web-fonts/src/util.ts
@@ -0,0 +1,15 @@
+// eslint-disable-next-line node/prefer-global/process
+export const isNode = typeof process !== 'undefined' && process.stdout && !process.versions.deno
+
+export async function resolveDownloadDir(downloadDir?: string | (() => string)) {
+ if (!isNode)
+ return ''
+
+ if (typeof downloadDir === 'function')
+ return downloadDir()
+
+ const { resolve } = await import('node:path')
+ const { cwd } = await import('node:process')
+
+ return typeof downloadDir === 'string' ? resolve(cwd(), downloadDir) : `${cwd()}/public/unocss-fonts`
+}
diff --git a/packages/vite/build.config.ts b/packages/vite/build.config.ts
index 074e34210c..f0e94d89a0 100644
--- a/packages/vite/build.config.ts
+++ b/packages/vite/build.config.ts
@@ -9,5 +9,10 @@ export default defineBuildConfig({
declaration: true,
externals: [
'vite',
+ 'ofetch',
+ 'destr',
+ 'ufo',
+ '@unocss/preset-web-fonts/local-font',
+ '@unocss/preset-web-fonts/remote-font',
],
})
diff --git a/packages/vite/src/index.ts b/packages/vite/src/index.ts
index e3093f1929..5636ca9bdc 100644
--- a/packages/vite/src/index.ts
+++ b/packages/vite/src/index.ts
@@ -12,6 +12,7 @@ import { ConfigHMRPlugin } from './config-hmr'
import type { VitePluginConfig } from './types'
import { createTransformerPlugins } from './transformers'
import { createDevtoolsPlugin } from './devtool'
+import { createWebFontPlugins } from './web-fonts'
export * from './types'
export * from './modes/chunk-build'
@@ -43,6 +44,7 @@ export default function UnocssPlugin(
ConfigHMRPlugin(ctx),
...createTransformerPlugins(ctx),
...createDevtoolsPlugin(ctx, inlineConfig),
+ ...createWebFontPlugins(ctx),
{
name: 'unocss:api',
api: {
diff --git a/packages/vite/src/web-fonts.ts b/packages/vite/src/web-fonts.ts
new file mode 100644
index 0000000000..1ceeee2d1e
--- /dev/null
+++ b/packages/vite/src/web-fonts.ts
@@ -0,0 +1,46 @@
+import type { Plugin } from 'vite'
+import type { Preset, UnocssPluginContext } from '@unocss/core'
+
+export function createWebFontPlugins(ctx: UnocssPluginContext): Plugin[] {
+ return [
+ {
+ name: `unocss:web-fonts-local:dev`,
+ enforce: 'pre',
+ async configResolved(config) {
+ const webFontPreset = await lookupPreset(ctx, '@unocss/preset-web-fonts')
+ if (!webFontPreset || !webFontPreset.options?.downloadLocally)
+ return
+
+ const [
+ { defaultFontFolder, useLocalFont },
+ { getRemoteFontsCSS },
+ { $fetch },
+ { resolve },
+ ] = await Promise.all([
+ import('@unocss/preset-web-fonts/local-font'),
+ import('@unocss/preset-web-fonts/remote-font'),
+ import('ofetch'),
+ import('node:path'),
+ ])
+
+ if (webFontPreset.options.downloadLocally === true)
+ webFontPreset.options.downloadLocally = {}
+
+ if (typeof webFontPreset.options.downloadLocally.downloadDir === 'undefined')
+ webFontPreset.options.downloadLocally.downloadDir = resolve(config.publicDir, defaultFontFolder)
+
+ if (typeof webFontPreset.options.downloadLocally.downloadBasePath === 'undefined')
+ webFontPreset.options.downloadLocally.downloadBasePath = config.base
+
+ const fontCSS = await getRemoteFontsCSS(webFontPreset.options.fontObject, { inlineImports: true, customFetch: $fetch })
+ await useLocalFont(fontCSS, webFontPreset.options.downloadLocally)
+ },
+ },
+ ]
+}
+
+async function lookupPreset>(ctx: UnocssPluginContext, presetName: P['name']) {
+ await ctx.ready
+ const preset: P | undefined = ctx.uno.config?.presets?.find(p => p.name === presetName) as any
+ return preset
+}
diff --git a/playground/src/App.vue b/playground/src/App.vue
index af282f7acf..c56d82299d 100644
--- a/playground/src/App.vue
+++ b/playground/src/App.vue
@@ -1,5 +1,5 @@
-
+
diff --git a/playground/src/components/HeaderBar.vue b/playground/src/components/HeaderBar.vue
index e6387edf1c..2a72acd934 100644
--- a/playground/src/components/HeaderBar.vue
+++ b/playground/src/components/HeaderBar.vue
@@ -23,7 +23,7 @@ function handleReset() {
@@ -36,7 +36,7 @@ function handleReset() {
-
+