Conversation
Introduces a `fonts` option to the plugin config with support for four providers: local(), google(), bunny(), and fontsource(). Build mode: resolves font families, emits assets, generates @font-face CSS with fallback metrics, and writes a fonts-manifest.json for Laravel consumption. Dev mode: eagerly resolves fonts on server start, writes an inline hot manifest, and serves font files via plugin middleware. Exported as a separate entry point at laravel-vite-plugin/fonts. Includes 80 new tests covering config, CSS generation, manifest building, caching, dev server, and CSS parsing.
The framework needs to filter font CSS by family without regex-parsing the generated output. Emit familyStyles (per-family @font-face + fallback CSS) and variables (shared :root block) alongside the existing full CSS reference so the framework can select fragments directly.
- Merge variant files sharing the same weight:style instead of overwriting, preserving all unicode-range subsets - Reject duplicate family names in config validation since the manifest is keyed by family name - Include configured subsets in Google/Bunny CSS2 request URLs - Extract shared manifest logic into resolveEntries helper
Generates a `.font-*` utility class per configured font family, derived from the CSS variable name (e.g. `--font-sans` → `.font-sans`). Classes reference the CSS variable so fallback chains are included automatically. Appended to both full CSS output and per-family `familyStyles` fragments so partial loading via `@fonts(['Inter'])` works correctly.
- Register font middleware in configureServer hook instead of the httpServer 'listening' callback so it runs before Vite's SPA fallback middleware, which was swallowing all font requests with 404 - Use createRequire from 'module' package for fontsource resolution since the bundled ESM output lacks require.resolve support - Fix fontsource CSS filename pattern to match the actual convention used by @fontsource packages (subset-weight.css, not subset-weight-style.css) - Emit absolute URL paths in font CSS and familyStyles so they resolve correctly when inlined into <style> tags in production builds
Replace nested FontConfig/FontProviderConfig model with flat FontDefinition where provider functions (google, bunny, fontsource, local) return complete definitions. Introduce alias-based keying for manifests and CSS variables, definition merging for incremental weight/style composition, selective preload control, and explicit fallback font stacks. Consolidate per-provider files into unified modules.
The local() provider now supports three shorthand modes in addition to explicit variants: - Single file: src pointing at one font file - Directory: src pointing at a directory for recursive discovery - Glob: src with a glob pattern to filter discovered files Weight and style are inferred from filenames when omitted from variant definitions. Files with the same weight+style but different formats are grouped into a single variant with format preference ordering.
- Fix mixed unicode-range variants silently dropping non-ranged files - Validate CSS variable names must be non-empty and start with "--" - Reject incompatible local font shape merges (src vs variants) - Fix false italic inference from filenames ending in "it" (e.g. Split) - Normalize explicit variant source format priority to match shorthand - Scope dev manifest cleanup per instance instead of module-global - Extract shared helpers in css.ts and cache.ts to reduce duplication
joetannenbaum
left a comment
There was a problem hiding this comment.
Some questions/things to consider. Let me know what you think.
| return undefined | ||
| } | ||
|
|
||
| const sizeAdjust = 1 |
There was a problem hiding this comment.
This is hardcoded to 1... what purpose does it serve?
| 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', | ||
| ] | ||
|
|
||
| function pickUserAgent(): string { |
There was a problem hiding this comment.
Why are we randomizing the user agent?
| } | ||
|
|
||
| const mime = FORMAT_MIME[entry.format] ?? 'application/octet-stream' | ||
| const data = fs.readFileSync(entry.source) |
There was a problem hiding this comment.
This blocks the event loop for every font request during development. For small woff2 files it's fine, but for OTF/TTF files that can be several MB, it'll cause noticeable lag. Should be fs.createReadStream(entry.source).pipe(res) or at minimum fs.readFile with a callback.
|
|
||
| res.setHeader('Content-Type', mime) | ||
| res.setHeader('Access-Control-Allow-Origin', '*') | ||
| res.setHeader('Cache-Control', 'public, max-age=31536000, immutable') |
There was a problem hiding this comment.
For dev shouldn't this be no-cache or a very short max-age?
| bunny: 'https://fonts.bunny.net/css2', | ||
| } | ||
|
|
||
| const activeManifestPaths = new Set<string>() |
There was a problem hiding this comment.
This is at the module level, if someone is running multiple Vite instances this will get muddy. Claude suggests something like the following to fix:
// Delete these two module-level lines:
// const activeManifestPaths = new Set<string>()
// let cleanupBound = false
// Then in configureServer, replace the activeManifestPaths/cleanupBound block with:
server.httpServer?.on('close', () => {
try {
if (fs.existsSync(hotManifestPath)) {
fs.rmSync(hotManifestPath)
}
} catch {
// Best-effort cleanup
}
})| async buildStart() { | ||
| if (resolvedConfig.command !== 'build') { | ||
| return | ||
| } |
There was a problem hiding this comment.
Why are we building the CSS twice, once with placeholders, once with the real content? Can we just do it once in generateBundle?
| for (const file of rangedFiles) { | ||
| const fileSrc = `url("${filePathMap.get(file.source) ?? file.source}") format("${file.format}")` | ||
|
|
||
| rules.push([ |
There was a problem hiding this comment.
Question: The output Vite's job? It might be, but it also might be the Blade directive's job? Maybe the plugin just stores the object and the PHP side consumes it and creates the output? Food for thought.
| } | ||
|
|
||
| export function generateFallbackFontFace( | ||
| family: string, |
Summary
@font-facerules, CSS variables, utility classes, and a manifest for the framework runtime/__laravel_vite_plugin__/fonts/with proper CORS and caching headers<style>tagsExamples
Basic usage
Single remote font with full API
Advanced local font