diff --git a/src/plugins/transform.ts b/src/plugins/transform.ts index 3cc4da5..00a4329 100644 --- a/src/plugins/transform.ts +++ b/src/plugins/transform.ts @@ -39,16 +39,29 @@ export const FontFamilyInjectionPlugin = (options: FontFamilyInjectionPluginOpti } } - // TODO: handle these edge cases - // 1. existing font-family in this scope - // 2. handle CSS custom properties - walk(parse(code), { + const ast = parse(code) + + // Collect existing `@font-face` declarations (to skip adding them) + const existingFontFamilies = new Set() + walk(ast, { + visit: 'Declaration', + enter (node) { + if (this.atrule?.name === 'font-face' && node.property === 'font-family') { + for (const family of extractFontFamilies(node)) { + existingFontFamilies.add(family) + } + } + } + }) + + // TODO: handle CSS custom properties + walk(ast, { visit: 'Declaration', enter (node) { if (node.property !== 'font-family' || this.atrule?.name === 'font-face') { return } for (const fontFamily of extractFontFamilies(node)) { - if (processedFontFamilies.has(fontFamily)) continue + if (processedFontFamilies.has(fontFamily) || existingFontFamilies.has(fontFamily)) continue processedFontFamilies.add(fontFamily) promises.push(addFontFaceDeclaration(fontFamily)) } diff --git a/test/parse.test.ts b/test/parse.test.ts index 6cbf9d7..259a912 100644 --- a/test/parse.test.ts +++ b/test/parse.test.ts @@ -2,28 +2,47 @@ import { describe, expect, it } from 'vitest' import { FontFamilyInjectionPlugin } from '../src/plugins/transform' describe('parsing', () => { - it('should declarations within `@font-face`', async () => { + it('should add declarations for `font-family`', async () => { + expect(await transform(`:root { font-family: 'CustomFont' }`)) + .toMatchInlineSnapshot(` + "@font-face { + font-family: 'CustomFont'; + src: url("/customfont.woff2") format(woff2); + font-display: swap; + } + :root { font-family: 'CustomFont' }" + `) + }) + + it('should skip processing declarations within `@font-face`', async () => { expect(await transform(`@font-face { font-family: 'CustomFont' }`)) .toMatchInlineSnapshot(`undefined`) }) - it('should add declarations for `font-family`', async () => { - expect(await transform(`:root { font-family: 'CustomFont' }`)) + it('should ignore any @font-face already in scope', async () => { + expect(await transform([ + `@font-face { font-family: 'ScopedFont'; src: local("ScopedFont") }`, + `:root { font-family: 'ScopedFont' }`, + `:root { font-family: 'CustomFont' }`, + ].join('\n'))) .toMatchInlineSnapshot(` "@font-face { font-family: 'CustomFont'; - src: url("/font.woff2") format(woff2); + src: url("/customfont.woff2") format(woff2); font-display: swap; } + @font-face { font-family: 'ScopedFont'; src: local("ScopedFont") } + :root { font-family: 'ScopedFont' } :root { font-family: 'CustomFont' }" `) }) }) - +const slugify = (str: string) => str.toLowerCase().replace(/[^\d\w]/g, '-') async function transform (css: string) { - const plugin = FontFamilyInjectionPlugin({ resolveFontFace: () => ({ src: '/font.woff2' }) }) - .raw({}, { framework: 'vite' }) as any + const plugin = FontFamilyInjectionPlugin({ + resolveFontFace: (family) => ({ src: `/${slugify(family)}.woff2` }) + }).raw({}, { framework: 'vite' }) as any const result = await plugin.transform(css) return result?.code