vite-plugin-llm
Mock data is one of those chores that never quite justifies its own effort.
Mock data is one of those chores that never quite justifies its own effort. You need a realistic-looking user object, or a plausible list of orders, or some filler text that isn't literally "lorem ipsum" because you want the UI to feel real while you're building it, and writing that data by hand, every time, for every component, is exactly tedious enough that people either skip it and ship with obviously fake placeholders, or spend fifteen minutes on something that shouldn't take fifteen minutes.
vite-plugin-llm is my attempt to make that fifteen minutes into zero. Write a comment describing what you want, something like // @llm-mock: a list of 5 recent orders with customer names and totals, and the plugin catches that comment during Vite's build transform, sends the description to a local model, and splices the generated value directly into your source as const _mock = ...;. You write intent. The plugin writes the filler.
Why local, and why that specific default
The default endpoint is http://localhost:11434 (Ollama's default port, model llama3). That's not an arbitrary default; it's a deliberate one. Mock data generation happens constantly during active development, potentially dozens of times an hour while you're iterating on a component. Routing that through a paid API, even a cheap one, turns an idle convenience into a running cost that scales with how much you're actually building. That's backwards. The whole point of mock data is that it doesn't need to be good, just plausible, and a small local model running on your own machine is more than capable of "plausible" without a bill attached. I built this while experimenting heavily with local models generally, and this plugin is really that experimentation finding one concrete, low-stakes use case where "good enough and free" beats "excellent and metered."
There's on-disk caching too: an .llm-cache/mocks.json file, keyed by an MD5 hash of the directive text, so identical mock requests across rebuilds don't re-hit the model every single time. A dev-server middleware route, /__llm/refresh, deletes that cache on request, for the moments you actually want fresh data instead of the same cached mock you've been staring at for an hour.
Where I cut a corner, on purpose, and where it bit me
The transform logic finds the @llm-mock comment and replaces "the next line" with the generated value. Not the next expression, not the next statement parsed properly: the next line, found with a second regex built from the escaped directive text. That's a deliberate simplification: writing a real AST transform, parsing the surrounding code properly and replacing exactly the right node, would have taken meaningfully longer to build than the regex version, for a plugin whose entire purpose is saving me time on something low-stakes. I chose the fast, fragile version because the cost of it being fragile is low. Worst case, a mock doesn't get replaced correctly and I notice immediately because the UI looks wrong, not because something silently corrupted real data.
Where it does bite: multi-line code immediately after the directive, or anything nested in a way the regex doesn't expect, breaks the replacement. It assumes exactly one line follows the comment and clobbers that line specifically. I've hit this a handful of times: write the directive, the next line happens to wrap onto two lines because of formatting, and the replacement lands somewhere I didn't intend. Annoying, immediately visible, and never something I've considered worth fixing properly, because the fix is "write the directive right before a genuinely single-line assignment," which is a habit, not a bug report.
What happens when the model fails
If generation fails (model's not running, request times out, whatever) the plugin doesn't crash the build. It swaps in null /* llm mock failed */ and moves on. I went back and forth on this. Failing loudly would catch the problem immediately; failing silently into a clearly-marked placeholder keeps the dev server running, which matters more to me in practice, because a build that halts because Ollama wasn't running yet is a worse interruption than a component temporarily rendering null with an obvious comment explaining why. The tradeoff is that it's possible to not notice a failed mock for longer than you'd like, if you're not paying attention to the console. That's a real cost. I've decided it's a smaller cost than a build that stops entirely over something as low-stakes as fake data not generating.
What it's actually for
This isn't infrastructure I lean on for anything that matters. It's a convenience for the parts of building a UI that are genuinely boring: populating a table with data that looks real enough to judge the layout by, without hand-authoring twenty rows of it every time I want to see how a component handles a realistic dataset. It's a small plugin for a small annoyance, built around a local model because the annoyance recurs often enough that paying per-call for it would have been the wrong trade. No tests, no examples folder, same as most of this batch, because the actual test is whether the UI looks right after I write the directive, and I can see that with my own eyes faster than any test suite would tell me.
The moment I realized the cache mattered more than the generation
I underestimated caching when I first built this. My assumption was that the model call itself would be the slow part, and the cache was a nice-to-have on top. What actually happened, the first time I used this on a real component with a handful of directives in it, was that every single hot reload (every time I saved the file while tweaking unrelated styling) re-triggered the transform, which meant re-triggering the model call for every directive on that page, every save, even though nothing about the mock data itself had changed. Without the cache, iterating on a component's CSS while it happened to contain an @llm-mock directive turned into a multi-second pause on every keystroke-adjacent save. The MD5-keyed cache fixed that completely: same directive text, same hash, same cached value, no model call at all after the first generation. It turned the plugin from "occasionally useful, often annoying" into something I stopped noticing was even running, which is exactly the bar a build-tool plugin should clear.
What I'd actually want if I rebuilt this properly
A real AST-based transform instead of the line-following regex is the obvious fix, and I've thought about what it would take: parse the file with the same tooling Vite's own JS/TS handling already uses internally, find the actual statement or expression following the directive comment as a proper syntax node, and replace exactly that node regardless of how many lines it spans. That would close the multi-line fragility completely. I haven't done it, mostly because the plugin already does what I need for the low-stakes mock-data use case it was built for, and the complexity of a real AST transform felt disproportionate to a tool I only ever reach for during early UI scaffolding, not anywhere near production code paths.
Full stack developer. Founder of Yashveer Labs. Write the intent. Let a small local model handle the filler.
Start a conversation about this.
Whether it's vite-plugin-llm itself or the next system worth building, the lab is reachable.