diff --git a/package.json b/package.json index d469fd97..c7effaa0 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "resolutions": { "@nuxt/fonts>fontaine": "latest", "fontaine": "workspace:*", - "fontless": "workspace:*" + "fontless": "workspace:*", + "unifont": "0.6.0" }, "simple-git-hooks": { "pre-commit": "npx lint-staged" diff --git a/packages/fontless/README.md b/packages/fontless/README.md index a4fedd6f..936765dc 100644 --- a/packages/fontless/README.md +++ b/packages/fontless/README.md @@ -79,7 +79,7 @@ fontless({ // Default font settings defaults: { - preload: true, + preload: { subsets: ['latin'] }, // select preload fonts by subset weights: [400, 700], styles: ['normal', 'italic'], fallbacks: { diff --git a/packages/fontless/examples/tailwind/vite.config.ts b/packages/fontless/examples/tailwind/vite.config.ts index 16b43951..ad38cf0e 100644 --- a/packages/fontless/examples/tailwind/vite.config.ts +++ b/packages/fontless/examples/tailwind/vite.config.ts @@ -8,6 +8,12 @@ export default defineConfig({ tailwindcss(), fontless({ provider: 'google', + families: [ + { + name: 'Geist', + preload: { subsets: ['latin'] }, + } + ] }), ], }) diff --git a/packages/fontless/examples/vanilla-app/package.json b/packages/fontless/examples/vanilla-app/package.json index 4364df54..ad887f4e 100644 --- a/packages/fontless/examples/vanilla-app/package.json +++ b/packages/fontless/examples/vanilla-app/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "vite", - "build": "tsc && vite build", + "build": "vite build", "preview": "vite preview" }, "devDependencies": { diff --git a/packages/fontless/src/types.ts b/packages/fontless/src/types.ts index 049fdf2b..e0909686 100644 --- a/packages/fontless/src/types.ts +++ b/packages/fontless/src/types.ts @@ -38,11 +38,7 @@ export interface FontFamilyOverrides { name: string /** Inject `@font-face` regardless of usage in project. */ global?: boolean - /** - * Enable or disable adding preload links to the initially rendered HTML. - * This is true by default for the highest priority format unless a font is subsetted (to avoid over-preloading). - */ - preload?: boolean + preload?: PreloadOption // TODO: // as?: string @@ -66,6 +62,17 @@ export interface FontFamilyManualOverride extends FontFamilyOverrides, RawFontFa type ProviderOption = ((options: any) => Provider) | string | false +/** + * Enable adding preload links to the initially rendered HTML. + * With `subsets`, you can specify which subsets to preload. + * @default false + * @example { subsets: ['latin'] } + */ +type PreloadOption + = | boolean + | { subsets: string[] } + | ((fontFamily: string, font: FontFaceData) => boolean) + export interface FontlessOptions { /** * Specify overrides for individual font families. @@ -85,7 +92,7 @@ export interface FontlessOptions { */ families?: Array defaults?: Partial<{ - preload: boolean + preload?: PreloadOption weights: Array styles: ResolveFontOptions['styles'] subsets: ResolveFontOptions['subsets'] diff --git a/packages/fontless/src/utils.ts b/packages/fontless/src/utils.ts index 260d01a6..8e487829 100644 --- a/packages/fontless/src/utils.ts +++ b/packages/fontless/src/utils.ts @@ -27,7 +27,9 @@ export interface FontFamilyInjectionPluginOptions { resolveFontFace: (fontFamily: string, fallbackOptions?: { fallbacks: string[], generic?: GenericCSSFamily }) => Awaitable dev: boolean processCSSVariables?: boolean | 'font-prefixed-only' + /** @deprecated use `filterFontsToPreload` instead */ shouldPreload: (fontFamily: string, font: FontFaceData) => boolean + filterFontsToPreload?: (fontFamily: string, fonts: FontFaceData[]) => FontFaceData[] fontsToPreload: Map> } @@ -96,8 +98,15 @@ export async function transformCSS(options: FontFamilyInjectionPluginOptions, co let insertFontFamilies = false const [topPriorityFont] = result.fonts.sort((a, b) => (a.meta?.priority || 0) - (b.meta?.priority || 0)) + const fontsToPreload: FontFaceData[] = [] if (topPriorityFont && options.shouldPreload(fontFamily, topPriorityFont)) { - const fontToPreload = topPriorityFont.src.find((s): s is RemoteFontSource => 'url' in s)?.url + fontsToPreload.push(topPriorityFont) + } + if (options.filterFontsToPreload) { + fontsToPreload.push(...options.filterFontsToPreload(fontFamily, result.fonts)) + } + for (const font of fontsToPreload) { + const fontToPreload = font.src.find((s): s is RemoteFontSource => 'url' in s)?.url if (fontToPreload) { const urls = options.fontsToPreload.get(id) || new Set() options.fontsToPreload.set(id, urls.add(fontToPreload)) diff --git a/packages/fontless/src/vite.ts b/packages/fontless/src/vite.ts index a3dffd27..9234eefb 100644 --- a/packages/fontless/src/vite.ts +++ b/packages/fontless/src/vite.ts @@ -47,9 +47,23 @@ export function fontless(_options?: FontlessOptions): Plugin { cssTransformOptions = { processCSSVariables: options.processCSSVariables, - shouldPreload(fontFamily, _fontFace) { + shouldPreload: () => false, + filterFontsToPreload(fontFamily, fonts) { const override = options.families?.find(f => f.name === fontFamily) - return override?.preload ?? options.defaults?.preload ?? false + const preload = override?.preload ?? options.defaults?.preload + // pick by priority (old behavior) + if (preload === true) { + return fonts.sort((a, b) => (a.meta?.priority || 0) - (b.meta?.priority || 0)).slice(0, 1) + } + // filter by function + if (typeof preload === 'function') { + return fonts.filter(f => preload(fontFamily, f)) + } + // filter by subset + if (preload && 'subsets' in preload) { + return fonts.filter(f => f.meta?.subset && preload.subsets.includes(f.meta.subset)) + } + return [] }, fontsToPreload: new Map(), dev: config.mode === 'development', diff --git a/packages/fontless/test/e2e.spec.ts b/packages/fontless/test/e2e.spec.ts index 5c7ec3fe..e9c647ed 100644 --- a/packages/fontless/test/e2e.spec.ts +++ b/packages/fontless/test/e2e.spec.ts @@ -56,6 +56,11 @@ describe.each(fixtures)('e2e %s', (fixture) => { const woff2 = content.indexOf('format(woff2)') expect(woff >= 0 && woff2 >= 0).toBe(true) expect(woff).lessThan(woff2) + const html = files.find(file => file.endsWith('.html'))! + const htmlContent = await readFile(join(outputDir!, html), 'utf-8') + expect(htmlContent).toContain('rel="preload" as="font"') + expect(htmlContent).toContain('.woff2"') // woff2 is preloaded + expect(htmlContent).not.toContain('.woff"') // woff is not preloaded } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62139fda..49f2749d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,7 @@ overrides: '@nuxt/fonts>fontaine': latest fontaine: workspace:* fontless: workspace:* + unifont: 0.6.0 importers: @@ -150,7 +151,7 @@ importers: specifier: ^1.6.1 version: 1.6.1 unifont: - specifier: ^0.6.0 + specifier: 0.6.0 version: 0.6.0 unstorage: specifier: ^1.17.1 @@ -10832,9 +10833,6 @@ packages: unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} - unifont@0.4.1: - resolution: {integrity: sha512-zKSY9qO8svWYns+FGKjyVdLvpGPwqmsCjeJLN1xndMiqxHWBAhoWDMYMG960MxeV48clBmG+fDP59dHY1VoZvg==} - unifont@0.6.0: resolution: {integrity: sha512-5Fx50fFQMQL5aeHyWnZX9122sSLckcDvcfFiBf3QYeHa7a1MKJooUy52b67moi2MJYkrfo/TWY+CoLdr/w0tTA==} @@ -14286,7 +14284,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 ufo: 1.6.1 - unifont: 0.4.1 + unifont: 0.6.0 unplugin: 2.3.10 unstorage: 1.17.1(db0@0.3.2)(ioredis@5.7.0) transitivePeerDependencies: @@ -24233,11 +24231,6 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 - unifont@0.4.1: - dependencies: - css-tree: 3.1.0 - ohash: 2.0.11 - unifont@0.6.0: dependencies: css-tree: 3.1.0