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(web-fonts): add downloadLocally options #3723

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -25,3 +25,4 @@ interactive/public/shiki
.svelte-kit
.eslintcache
vite.config.ts.timestamp-*
unocss-fonts/
3 changes: 2 additions & 1 deletion examples/astro/src/layouts/main.astro
Expand Up @@ -8,8 +8,9 @@ const { content } = Astro.props;
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>{content.title}</title>
<link rel="stylesheet" href="/unocss-fonts/fonts.css" />
</head>
<body>
<body class="font-sans">
<slot />
</body>
</html>
3 changes: 2 additions & 1 deletion examples/astro/src/pages/index.astro
Expand Up @@ -13,9 +13,10 @@ import Button from '../components/Button.astro';
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<title>Astro + UnoCSS</title>
<link rel="stylesheet" href="/unocss-fonts/fonts.css" />
</head>

<body>
<body class="font-sans">
<div class="grid place-items-center h-screen content-center">
<Button>UnoCSS Button in Astro!</Button>
<a href="/markdown-page" class="p-4 underline">Markdown is also supported...</a>
Expand Down
14 changes: 14 additions & 0 deletions examples/astro/uno.config.ts
Expand Up @@ -2,10 +2,16 @@ import {
defineConfig,
presetIcons,
presetUno,
presetWebFonts,
transformerDirectives,
} from 'unocss'

export default defineConfig({
theme: {
fontFamily: {
sans: 'sans-serif',
},
},
shortcuts: [
{ 'i-logo': 'i-logos-astro w-6em h-6em transform transition-800' },
],
Expand All @@ -20,5 +26,13 @@ export default defineConfig({
'vertical-align': 'middle',
},
}),
presetWebFonts({
downloadLocally: true,
provider: 'google',
fonts: {
sans: 'Lato',
serif: 'Merriweather',
},
}),
],
})
2 changes: 1 addition & 1 deletion examples/nuxt3/app.vue
@@ -1,5 +1,5 @@
<template>
<main class="py-20 px-12 text-center">
<main class="py-20 px-12 text-center font-sans">
<span text="blue 5xl hover:red" cursor="default">Hello Nuxt 3</span>
<br>
<div i-carbon-car text-4xl inline-block />
Expand Down
13 changes: 13 additions & 0 deletions examples/nuxt3/nuxt.config.ts
Expand Up @@ -6,6 +6,19 @@ export default defineNuxtConfig({
attributify: true,
icons: true,
components: false,
theme: {
fontFamily: {
sans: 'sans-serif',
},
},
webFonts: {
downloadLocally: true,
provider: 'google',
fonts: {
sans: 'Lato',
serif: 'Merriweather',
},
},
shortcuts: [
['btn', 'px-4 py-1 rounded inline-block bg-teal-600 text-white cursor-pointer hover:bg-teal-700 disabled:cursor-default disabled:bg-gray-600 disabled:opacity-50'],
],
Expand Down
1 change: 1 addition & 0 deletions examples/sveltekit/package.json
Expand Up @@ -18,6 +18,7 @@
"@unocss/extractor-svelte": "link:../../packages/extractor-svelte",
"@unocss/preset-icons": "link:../../packages/preset-icons",
"@unocss/preset-uno": "link:../../packages/preset-uno",
"@unocss/preset-web-fonts": "link:../../packages/preset-web-fonts",
"svelte": "^4.2.15",
"svelte-check": "^3.6.9",
"tslib": "^2.6.2",
Expand Down
2 changes: 1 addition & 1 deletion examples/sveltekit/src/app.html
Expand Up @@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body data-sveltekit-prefetch>
<body data-sveltekit-prefetch class="font-sans">
<div>%sveltekit.body%</div>
</body>
</html>
4 changes: 4 additions & 0 deletions examples/sveltekit/src/routes/+layout.svelte
Expand Up @@ -13,6 +13,10 @@
$: span = red ? 'Normal' : 'Red'
</script>

<svelte:head>
<link rel="stylesheet" href="/unocss-fonts/fonts.css" />
</svelte:head>

<main class="text-center p-1em my-0 mx-auto">
<span class="logo"></span>

Expand Down
2 changes: 1 addition & 1 deletion examples/sveltekit/src/routes/+page.svelte
Expand Up @@ -7,4 +7,4 @@

<Go /><br />

<a href="/about" class="text-green-600 fw-bold">About</a><br />
<a href="/about" class="text-green-600 fw-bold font-serif">About</a><br />
15 changes: 14 additions & 1 deletion examples/sveltekit/uno.config.ts
@@ -1,10 +1,15 @@
import { defineConfig } from 'unocss'
import { defineConfig, presetWebFonts } from 'unocss'
import extractorSvelte from '@unocss/extractor-svelte'
import presetIcons from '@unocss/preset-icons'
import presetUno from '@unocss/preset-uno'
import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders'

export default defineConfig({
theme: {
fontFamily: {
sans: 'sans-serif',
},
},
extractors: [
extractorSvelte(),
],
Expand Down Expand Up @@ -32,5 +37,13 @@ export default defineConfig({
'vertical-align': 'middle',
},
}),
presetWebFonts({
downloadLocally: true,
provider: 'google',
fonts: {
sans: 'Lato',
serif: 'Merriweather',
},
}),
],
})
4 changes: 4 additions & 0 deletions packages/astro/build.config.ts
Expand Up @@ -8,5 +8,9 @@ export default defineBuildConfig({
declaration: true,
externals: [
'astro',
'ofetch',
'destr',
'ufo',
'@unocss/preset-web-fonts/local-font',
],
})
3 changes: 3 additions & 0 deletions packages/astro/src/index.ts
Expand Up @@ -7,6 +7,7 @@ import type { UserConfigDefaults } from '@unocss/core'
import type { Plugin } from 'vite'
import { normalizePath } from 'vite'
import { RESOLVED_ID_RE } from '../../shared-integration/src/layers'
import { configureWebFontPreset } from './web-fonts'

const UNO_INJECT_ID = 'uno-astro'

Expand Down Expand Up @@ -97,6 +98,8 @@ export default function UnoCSSAstroIntegration<Theme extends object>(
if (injectExtra.length > 0)
injects.push(...injectExtra)

await configureWebFontPreset(config, defaults)

updateConfig({
vite: {
plugins: [AstroVitePlugin({
Expand Down
21 changes: 21 additions & 0 deletions 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<P extends Preset<any>>(options: UserConfigDefaults, presetName: P['name']) {
const preset: P | undefined = (options.presets || []).flat().find(p => p.name === presetName) as any
return preset
}
1 change: 1 addition & 0 deletions packages/nuxt/build.config.ts
Expand Up @@ -8,5 +8,6 @@ export default defineBuildConfig({
declaration: true,
externals: [
'@nuxt/schema',
'pathe',
],
})
4 changes: 4 additions & 0 deletions packages/nuxt/src/index.ts
Expand Up @@ -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 }

Expand Down Expand Up @@ -38,6 +39,9 @@ export default defineNuxtModule<UnocssNuxtOptions>({
// preset shortcuts
resolveOptions(options)

// configure local webfonts preset
await configureWebFontPreset(nuxt, options)

options.mode ??= 'global'
const InjectModes: VitePluginConfig['mode'][] = ['global', 'dist-chunk']

Expand Down
23 changes: 23 additions & 0 deletions 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('\\', '/'))
Copy link
Member

@userquin userquin Apr 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should use unshift?

}
}

function lookupPreset<P extends Preset<any>>(options: UnocssNuxtOptions, presetName: P['name']) {
const preset: P | undefined = (options.presets || []).flat().find(p => p.name === presetName) as any
return preset
}
2 changes: 2 additions & 0 deletions packages/preset-web-fonts/build.config.ts
Expand Up @@ -3,6 +3,8 @@ import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
entries: [
'src/index',
'src/local-font',
'src/remote-font',
],
clean: true,
declaration: true,
Expand Down
16 changes: 16 additions & 0 deletions packages/preset-web-fonts/package.json
Expand Up @@ -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"
Expand Down
67 changes: 67 additions & 0 deletions 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' })
}