npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vue-onigiri

v0.3.2

Published

Serialize and deserialize Vue component trees (VNodes) for Vue Server Components and cross-application component sharing

Downloads

410

Readme

vue-onigiri 🍙

npm version npm downloads

⚠️ This is a proof of concept.

Vue Onigiri brings React Server Components-style rendering to Vue. Components render on the server into a transferable AST; the client deserializes that AST back into VNodes, and only components marked with v-load-client ship their JS to the browser.

Features

  • Server Components — render on the server, send a serialized VNode tree to the client
  • Selective hydration — only components tagged with v-load-client are loaded client-side
  • Slot support — named and scoped slots survive serialization
  • Async / Suspense — async components and <Suspense> boundaries are preserved
  • Compile-time chunk resolutionv-load-client paths are resolved during compilation, no runtime tagging required
  • Bundler-agnostic — works with Vite by default; pass renderOnigiri(ast, { importFn }) to plug in any loader

Installation

npm install vue-onigiri
# or: pnpm add vue-onigiri / yarn add vue-onigiri / bun add vue-onigiri

Quick Start

1. Configure Vite

// vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { onigiriCompilerPlugin, onigiriManifestPlugin } from "vue-onigiri";

export default defineConfig({
  plugins: [vue(), onigiriCompilerPlugin(), onigiriManifestPlugin()],
});

2. Mark client-loaded components

Add v-load-client to any component that should hydrate on the client. The component must be statically imported in the same SFC (or registered through additionalImports / the Nuxt module):

<template>
  <div>
    <h1>Rendered on the server</h1>
    <Counter v-load-client />
  </div>
</template>

<script setup>
import Counter from "./Counter.vue";
</script>

The compiler reads the import and inlines /components/Counter.vue into the serialized payload. Components without v-load-client are rendered on the server and inlined — their source never reaches the browser.

3. Serialize on the server

import { serializeApp } from "vue-onigiri/runtime/serialize";
import { createSSRApp } from "vue";
import App from "./App.vue";

const app = createSSRApp(App);
const data = await serializeApp(app, undefined, { url: req.url });
// send `data` to the client (inlined in HTML, JSON endpoint, etc.)

4. Render on the client

import { renderOnigiri } from "vue-onigiri/runtime/deserialize";
import { createApp } from "vue";

const app = createApp({
  setup: () => () => renderOnigiri(data),
});
app.mount("#app");

Wrap the mount point in <Suspense> if your tree contains v-load-client components — each loader uses its own internal <Suspense>, but a top-level boundary keeps the initial render hydration-safe.

Vite Plugins

onigiriCompilerPlugin(options?)

Generates the per-SFC __onigiriRender function from each <template>. This is the only plugin doing real codegen work.

interface OnigiriCompilerOptions {
  /** @default true */
  sourceMap?: boolean;
  /**
   * Predicate for native custom elements / web components. Tags it
   * returns `true` for skip the Vue-component dispatch path and emit
   * as plain HTML. Mirrors Vue's `CompilerOptions.isCustomElement`.
   */
  isCustomElement?: (tag: string) => boolean;
  /**
   * Tag → root-relative module path for components the SFC doesn't
   * import statically. Lets `v-load-client` resolve to the right
   * chunk for Nuxt auto-imports, globally-registered components, or
   * any other case the compiler can't see in `<script>`. Pass either
   * a static map or a getter (re-evaluated per transform).
   */
  additionalImports?:
    | Record<string, string>
    | Map<string, string>
    | (() => Record<string, string> | Map<string, string>);
}

onigiriManifestPlugin(options?)

Emits the virtual:onigiri/manifest virtual module that the runtime loader imports. It exposes an importFn(src, exportName?) that resolves a root-relative .vue path via import.meta.glob.

interface OnigiriManifestPluginOptions {
  /**
   * Glob (relative to root) for the **server** lazy-load fallback.
   * Default: `/**\/*.vue`. Set to `false` to disable.
   */
  serverInclude?: string | false;
  /**
   * Glob for the **client** lazy-load fallback. Default: `false`.
   * Exposing `import.meta.glob` to the browser leaks every matching
   * file path into the bundle, so the safer default is to pass a
   * custom `importFn` via `renderOnigiri(ast, { importFn })`. Only set
   * this if you have client islands that aren't reachable through your
   * app's static import graph; scope it as narrowly as possible.
   */
  clientInclude?: string | false;
  /**
   * Force a no-glob manifest in **all** environments. Required for
   * bundlers that can't preprocess `import.meta.glob` or compile
   * `.vue` imports (e.g. Nitro's prerender rollup).
   */
  stub?: boolean;
}

Nuxt

Nuxt integrates onigiri directly: wire is handled inside Nuxt core, which feeds its component registry into the compiler's additionalImports so auto-imported components work with v-load-client without further setup. No separate module to install.

API Reference

serializeApp(app, slots?, ssrContext?)

Serialize an entire Vue app instance.

import { serializeApp } from "vue-onigiri/runtime/serialize";

const data = await serializeApp(app, undefined, { url: "/page" });

serializeComponent(component, props?, slots?, ssrContext?)

Serialize a single component without mounting an app.

import { serializeComponent } from "vue-onigiri/runtime/serialize";

const data = await serializeComponent(MyComponent, { title: "Hello" });

renderOnigiri(data)

Deserialize a payload back into a VNode tree.

import { renderOnigiri } from "vue-onigiri/runtime/deserialize";

const vnode = renderOnigiri(data);

renderOnigiri(ast, { importFn? })

Per-render-tree override for the chunk-loading function. Wins over the built-in virtual:onigiri/manifest. Use this when:

  • you need full control over how chunk paths map to modules (CDN-served bundles, federation, custom path normalization)
  • you don't have a Vite-managed manifest at all (non-Vite consumers, hand-rolled SSR, tests)

The fn is forwarded as a prop to every vue-onigiri:component-loader the AST contains, so nested v-load-client targets inherit it automatically.

import { renderOnigiri } from "vue-onigiri/runtime/deserialize";

renderOnigiri(ast, {
  importFn: async (src, exportName = "default") => {
    const mod = await myCustomLoader(src);
    return mod[exportName] ?? mod.default ?? mod;
  },
});

To centralize this in a Vue plugin pattern, write a tiny wrapper component that closures the importFn and forwards ast into renderOnigiri.

How It Works

  1. Compile timeonigiriCompilerPlugin generates a per-SFC __onigiriRender function. For <X v-load-client />, the chunk path is resolved from the SFC's static imports (or additionalImports) and embedded as a literal string. Unresolvable targets fail compilation with an explicit error.
  2. Server renderserializeApp walks the rendered tree. Server components inline as HTML/AST; client components emit a marker [Component, props, chunkPath, exportName, slots].
  3. Client renderrenderOnigiri recreates the VNode tree. Each Component marker mounts a loader that wraps defineAsyncComponent in its own <Suspense>, so hydration matches the server's empty fallback before swapping in the real component.
Server: VNode tree → serialize → AST + client markers
Client: AST → deserialize → VNode tree (lazy chunks resolved via importFn)

Limitations

  • Proof of concept — API is unstable, not production-ready.
  • v-load-client requires compile-time path resolution: the target component must be statically imported in the SFC, or registered through additionalImports (Nuxt module handles this automatically for auto-imported components).
  • <component :is="x" v-load-client /> with a runtime is value isn't supported — the compiler can't resolve the path at build time.
  • Components used outside an onigiri-compiled SFC (e.g. via Vue's vnode fallback path) can't carry v-load-client.
  • Scoped slots can't be passed into v-load-client components (the slot scope only exists on the client at runtime and can't be embedded in the frozen AST).
  • Payload size grows with tree size; deeply server-rendered pages produce larger responses than equivalent SSR HTML.

Migrating from 0.2.x

0.3 is a breaking change focused on shrinking the runtime surface:

  • Removed onigiriChunkPlugin / onigiriClientPlugin / onigiriServerPlugin and the onigiriPlugins() factory. The chunk plugin's job (tagging SFCs with __chunk / __export and self-registering into __ONIGIRI_REGISTRY__) is gone — paths are now resolved at compile time.
  • Removed the OnigiriRegistryEntry / OnigiriChunkInclude types and the registryInclude / registryExclude options.
  • Removed runtime Component.__chunk / Component.__export reads. The compiler always inlines a literal path; if it can't, it errors at build time.
  • Added additionalImports on onigiriCompilerPlugin — pass a map of { tag: path } for components the SFC doesn't import statically.
  • Renamed src/vite/chunk.tssrc/vite/manifest.ts (just an internal rename, not user-visible).

If your app worked because onigiriChunkPlugin was tagging components for you, the fix is one of:

  1. Add a static import for the component in the SFC where you use v-load-client.
  2. For Nuxt: upgrade to a Nuxt version that integrates onigiri directly (auto-imports work without further setup).
  3. For globally-registered components: pass them through onigiriCompilerPlugin({ additionalImports: { Foo: '/components/Foo.vue' } }).

Development

pnpm install
pnpm dev          # interactive playground
pnpm test         # vitest
pnpm build        # build the library
pnpm lint         # eslint
pnpm lint:fix

License

MIT — see LICENSE.

Credits

  • @antfu for naming this package 💖