fix(google-fonts): use closure variables instead of this for plugin sstate#53
fix(google-fonts): use closure variables instead of this for plugin sstate#53MehediH wants to merge 3 commits intocloudflare:mainfrom
Conversation
|
/bigbonk review |
There was a problem hiding this comment.
Review: fix(google-fonts): use closure variables instead of this for plugin state
The fix is correct. Vite extracts hook handler functions from plugin objects and calls them with a Rollup-compatible context object as this — not the plugin object itself. Using (this as any)._isBuild etc. would indeed reference undefined in strict mode. Wrapping the plugin in an IIFE and using closure variables is the standard and idiomatic pattern for Vite plugin state.
Two things to address:
-
Inconsistent indentation — the returned object body is indented at 6 spaces (matching the old top-level position), but being inside the IIFE it should be at 8 spaces. See inline comment.
-
The
vinext:image-importsplugin has the same bug — it uses(this as any)._dimCacheinside itsloadhook at line 2592. Theloadhook also receives a RollupLoadPluginContextasthis, not the plugin object. This means(this as any)._dimCacheis alwaysundefined, thecache.get()call at line 2593 always returnsundefined, and thecache.set()at line 2600 writes to a throwaway object. Every image dimension is re-read from disk on everyloadcall — the cache never works.
Since it's the identical pattern, please fix vinext:image-imports in the same PR. Specifically, the plugin at lines 2570-2668 should get the same IIFE + closure-variable treatment:
(() => {
const dimCache = new Map<string, { width: number; height: number }>();
return {
name: "vinext:image-imports",
// ... use dimCache directly instead of (this as any)._dimCache
} satisfies Plugin;
})()|
Review posted successfully. Here's a summary of the review: The fix is correct — Vite does not bind Two items flagged:
|
southpolesteve
left a comment
There was a problem hiding this comment.
Review: fix(google-fonts): use closure variables instead of this for plugin state
The fix itself is correct
The IIFE + closure variable pattern is the right approach. Vite extracts hook functions from plugin objects and calls them with a Rollup-compatible context as this, not the plugin object. The closure approach is idiomatic and avoids the issue entirely. The satisfies Plugin type assertion is a nice improvement over the old as Plugin & { ... } cast.
Blocking: Tests are broken
The test file tests/font-google.test.ts was not updated, and multiple tests will now fail or silently test the wrong thing.
The getGoogleFontsPlugin() helper (line 15) returns the plugin with a type assertion claiming _isBuild, _fontCache, and _cacheDir exist. After this PR, those properties are gone (they are closure variables). The as any cast hides this at compile time, but at runtime:
-
Tests that set
plugin._isBuild = truesilently test nothing. Setting a property on the plugin object has no effect because the transform hook reads the closure variableisBuild, which staysfalse. The transform returnsnullon theif (!isBuild) return nullcheck, and the test passes for the wrong reason. This affects:- "returns null for files without next/font/google imports" (line 287)
- "returns null for node_modules files" (line 297)
- "returns null for virtual modules" (line 306)
- "returns null for non-script files" (line 315)
- "returns null when import exists but no font constructor call" (line 324)
-
Tests that access
plugin._fontCachewill throw TypeError.plugin._fontCacheisundefined, so.clear()and.set()crash. This affects:- "transforms font call to include _selfHostedCSS during build" (line 334)
- "uses cached fonts on second call" (line 368)
- "handles multiple font imports in one file" (line 396)
- "skips font calls not from the import" (line 429)
- "fetches Inter font CSS and downloads woff2 files" (line 468)
The tests need to be rewritten to work with the closure-based approach. The standard pattern is to call configResolved with a mock config to set isBuild, and for the font cache, either:
- Export the IIFE factory so tests can access the closure variables (not great)
- Call
configResolved({ command: "build", root: "/tmp/test" })on the plugin to set the closure state, then rely on the transform's own caching (thefontCacheMap persists across calls within the same plugin instance) - Pre-populate the filesystem cache instead of the in-memory cache
The simplest fix: call plugin.configResolved({ command: "build", root: testDir } as any) to set isBuild and cacheDir via the hook, and for fontCache, accept that you can't pre-populate it from outside. Tests that need a pre-populated cache should either make a real fetch (like the existing network test) or mock fetchAndCacheFont.
Correction on previous review: vinext:image-imports does NOT have this bug
The previous review flagged vinext:image-imports as having the same this binding issue. That's incorrect. While the plugin declares _dimCache: imageImportDimCache on the plugin object (line 2760), the load hook on line 2777 uses const cache = imageImportDimCache (the module-level variable), not this._dimCache. The _dimCache property is dead code but harmless. The cache works correctly because it references the module-level variable directly.
That said, the dead _dimCache property and the as Plugin & { _dimCache: ... } cast on line 2853 should be cleaned up in a separate PR.
Summary
- Blocking: Tests must be updated. Multiple tests will either silently pass for the wrong reason or throw TypeErrors.
- Non-blocking (pre-existing): The
_dimCacheproperty onvinext:image-importsis dead code. Not introduced by this PR, should be cleaned up separately.
…tate
Vite does not bind `this` to the plugin object when calling hooks like
configResolved and transform.handler. Using `this._isBuild` etc. results
in a TypeError at startup ('Cannot set properties of undefined').
Replace the property-based state with closure variables so the plugin
works correctly regardless of how Vite invokes the hooks.
Tests were reaching into plugin internals (_isBuild, _fontCache, _cacheDir) which are now closure variables after the IIFE refactor. Updated to drive state through configResolved hook and mock fetch for cache tests.
8172379 to
8f5bec9
Compare
Vite does not bind
thisto the plugin object when calling hooks — it extracts the hook function and calls it directly. This meansthis._isBuildetc. inconfigResolvedandtransform.handlerrefers toundefinedin strict mode, crashing the dev server with:Fix: wrap the plugin in an IIFE and hold state in closure variables instead of as properties on the plugin object.