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

@lesmoutonssauvages/kitsu-plugin-kit

v0.0.5

Published

Authoring kit and Kitsu host runtime for injected frontend plugins (routes, Vuex, UI slots, provide/inject providers).

Readme

@lesmoutonssauvages/kitsu-plugin-kit

Authoring and host runtime for injected Kitsu frontend plugins: Vue bundles that run inside the Kitsu app (shared Vue / router / Vuex / i18n) instead of an iframe. Plugins declare routes, store modules, UI slots, and host provide overrides; the kit loads them into Kitsu.

Zou marks a plugin as injected when PLUGIN_FOLDER/<id>/frontend/dist/plugin.js exists. The user-context field injected: true is derived from that file — there is no injected flag in manifest.toml. Without plugin.js, Kitsu keeps the iframe integration and the plugin does not need this kit.

Navigation still comes from the manifest: frontend_studio_enabled (studio sidebar) and frontend_project_enabled (production topbar).

Package layout:

| Import | Role | | ------ | ---- | | @lesmoutonssauvages/kitsu-plugin-kit | Plugin authoring (definePlugin, store/context helpers) | | @lesmoutonssauvages/kitsu-plugin-kit/vite | Plugin Vite config (defineKitsuPluginConfig) | | @lesmoutonssauvages/kitsu-plugin-kit/host | Kitsu host (kitsuPlugins, PluginHost, PluginsSlot) | | @lesmoutonssauvages/kitsu-plugin-kit/host/vite | Host Vite helper (kitsuPluginsDev) |

This package uses npm (package-lock.json). Install with npm install. After editing src/, run npm run build (or npm run watch), and npm run lint / npm run format before committing.

The npm tarball ships src/ (not dist/). postinstall (and prepare for git/file: installs) runs npm run build so consumers get a local dist/ for exports. Plugin Vite serve aliases the kit to src/index.ts so __KITSU_PLUGIN_ID__ is applied under HMR.

Runtime contract

The host loads dist/plugin.js and expects a default export shaped like:

export default {
  async activate(context) {},
  async deactivate(context) {}
}

definePlugin() builds that object from a declarative definition. slots and providers are attached to the export and applied by the host (not via imperative APIs on setup context):

// src/index.ts
import { definePlugin } from '@lesmoutonssauvages/kitsu-plugin-kit'

import ActionPanelExtraAfter from './components/ActionPanelExtraAfter.vue'
import ActionPanelExtraBefore from './components/ActionPanelExtraBefore.vue'
import en from './locales/en'
import { myModule } from './stores/main'

export default definePlugin({
  messages: { en },
  store: myModule,
  slots: {
    'action-topbar-menu:before': ActionPanelExtraBefore,
    'action-topbar-menu:after': ActionPanelExtraAfter
  },
  providers: {
    'ComboboxStatus.sortedTaskStatusList': (list, productionId) =>
      [...list].sort((a, b) => /* custom order */ 0)
  },
  routes: {
    studio: [
      { path: '', name: 'index', component: () => import('./views/Studio.vue') }
    ],
    production: [
      { path: '', redirect: { name: 'overview' } },
      {
        path: 'overview',
        name: 'overview',
        component: () => import('./views/Overview.vue')
      }
    ]
  },
  setup(context) {},
  teardown(context) {}
})

Everything declared here is undone on deactivation (unload, HMR): routes are removed, the store module is unregistered, slots and host provider overrides are cleared, and context.onCleanup() callbacks run in reverse order.

The Zou plugin id comes from the parent manifest.toml via defineKitsuPluginConfig (injected as __KITSU_PLUGIN_ID__). Pass id to definePlugin only to override.

Routes

Routes are declared per scope and hang from the matching Kitsu route:

| Scope | Kitsu parent route | URL | | ------------ | -------------------------- | --------------------------------------------------------- | | studio | plugin | /plugins/<id>/... | | production | production-plugin | /productions/:production_id/plugins/<id>/... | | episode | episode-production-plugin| /productions/:production_id/episodes/:episode_id/plugins/<id>/... |

episode defaults to the production routes when omitted.

Navigate with Vue Router as in any Vue app. The host namespaces page names internally so they stay unique across plugins; authors keep the short names declared in definePlugin.

<script setup lang="ts">
import { RouterLink, useRoute, useRouter } from 'vue-router'

const router = useRouter()
const route = useRoute()

router.push({ name: 'list' })
// route.meta.pluginPage === 'list'
</script>

<template>
  <RouterLink :to="{ name: 'new' }">Create</RouterLink>
  <RouterLink :to="{ name: 'detail', params: { ticketId: ticket.id } }">
    {{ ticket.title }}
  </RouterLink>
  <RouterLink :to="{ name: 'bank', params: { production_id: row.id } }">
    {{ row.name }}
  </RouterLink>
</template>

Scope follows the params: episode_id → episode, else production_id → production, else studio. Current plugin_id is inherited. The current short name is on route.meta.pluginPage (route.name stays the global name Kitsu uses).

Do not reuse a Kitsu route name (login, open-productions, …) as a page name: those already exist, so the host leaves them alone.

A redirect: { name } in definePlugin is resolved against the plugin's own scope and keeps the current route params.

Store

store is a namespaced Vuex module. The host registers it as kitsu-plugin-<id> and adds a pluginId getter (the Zou plugin id). Use that in actions for /api/plugins/<id>/… — do not close over the id yourself.

export const myModule: Module<MyState, unknown> = {
  namespaced: true,
  state: () => ({ items: [] }),
  actions: {
    async load({ commit, getters }) {
      const response = await fetch(`/api/plugins/${getters.pluginId}/items`)
      if (response.ok) commit('setItems', await response.json())
    }
  }
}

In views, usePluginStore() talks to that module without the host path:

<script setup lang="ts">
import { usePluginStore } from '@lesmoutonssauvages/kitsu-plugin-kit'

const { state, commit, dispatch } = usePluginStore<MyState>()
commit('patch', { name: 'x' })
await dispatch('load')
</script>

A second module can still be registered in setup via context.registerStoreModule(name, module).

Context

usePluginContext() reads the context from the current route's meta:

<script setup lang="ts">
import { usePluginContext } from '@lesmoutonssauvages/kitsu-plugin-kit'

const context = usePluginContext()
// context.value.pluginId / manifest / store / productionId / storeModuleName / ...
</script>

Slots

Injected plugins can mount Vue components into named host UI slots. Kitsu exposes action-topbar-menu in ActionPanel.vue via:

<plugins-slot
  slot-name="action-topbar-menu"
  is="div"
  class="menu flexrow"
>
  <!-- host menu items -->
</plugins-slot>

is is optional. With is, contributions and host content are wrapped in that element or component (attrs such as class apply to it). Without is, PluginsSlot renders a fragment — no wrapper DOM node:

<plugins-slot slot-name="action-topbar-menu">
  <!-- host menu items -->
</plugins-slot>

Plugin keys are the slot name, optionally with a position suffix. Render order is before → (bare key | host content) → after. A bare key replaces the host default; :before / :after still wrap around that replacement:

export default definePlugin({
  slots: {
    'action-topbar-menu:before': ActionPanelExtraBefore,
    'action-topbar-menu': ActionPanelReplacement,
    'action-topbar-menu:after': ActionPanelExtraAfter
  }
})

Multiple plugins may fill the same key; they render in alphabetical order by pluginId. Slots are applied by the host from the declarative map only (no imperative registerSlot on setup). Registrations are removed on deactivate / HMR.

Providers

Plugins can override host provide/inject values via a declarative providers map. Keys are plain strings (<HostComponent>.<member>); values are opaque (unknown). The host applies them on activation — there is no register API on setup context.

export default definePlugin({
  providers: {
    'ComboboxStatus.sortedTaskStatusList': (list, productionId) =>
      [...list].sort((a, b) =>
        String(a.short_name).localeCompare(String(b.short_name))
      )
  }
})

Example: ComboboxStatus injects 'ComboboxStatus.sortedTaskStatusList' and, when a value is present, uses it to order task statuses instead of Kitsu’s default sortTaskStatuses. Last plugin to set a given key wins; on deactivate / HMR the value is cleared.

Build

// vite.config.ts
import { defineConfig } from 'vite'
import { defineKitsuPluginConfig } from '@lesmoutonssauvages/kitsu-plugin-kit/vite'

export default defineConfig(defineKitsuPluginConfig())

pluginId is read from ../manifest.toml (id = "…"). Pass pluginId only to override. Vite also injects __KITSU_PLUGIN_ID__ for definePlugin.

This produces a single dist/plugin.js (plus lazy chunks) with the stylesheet inlined, served by Zou at /api/plugins/<id>/frontend/plugin.js.

vue, vue-router, vuex and vue-i18n are not bundled: imports are rewritten to read the host instances from globalThis.__KITSU_SHARED__. They must still be installed as plugin dependencies at the versions the host uses — the build reads their export lists to generate the shims, and a version mismatch means missing or extra names.

Development

Each plugin runs its own Vite instance. Point Kitsu at those entries:

# terminal 1 — plugin frontend
cd path/to/my-plugin/frontend && npm run dev   # e.g. :5173

# terminal 2 — another plugin (optional)
cd path/to/other-plugin/frontend && npm run dev   # e.g. :5174

# terminal 3 — Kitsu
cd path/to/kitsu
KITSU_PLUGIN_DEV_URLS=\
my-plugin=http://127.0.0.1:5173,\
other-plugin=http://127.0.0.1:5174 \
  npm run dev

Origin-only URLs load /src/index.ts (the default plugin Vite entry). Pass a full path to override, e.g. my-plugin=http://127.0.0.1:5173/src/main.ts.

The host import()s those URLs (CORS is open on the plugin servers). Vue / router / store / i18n still come from the host via globalThis.__KITSU_SHARED__. Edits hot-reload on the plugin Vite; entry changes deactivate and reactivate the plugin in place.

Alternatively, compile plugin sources inside Kitsu's Vite (single graph):

KITSU_PLUGIN_DEV_PATHS=../../zou-plugins/*/frontend npm run dev

To exercise the real bundle instead, run npm run watch (or vite build --watch) in the plugin frontend and let Zou serve dist/; the host then reloads the built plugin.js.

HMR note: Zou still needs frontend/dist/plugin.js on disk (a built file or a stub) so the user context returns injected: true. Dev URLs only replace what is loaded, not the injected flag.

Testing locally

Use these checklists against Zou on feat/poc-injected-plugin-option with the plugin installed, and Kitsu running (npm run dev, optionally with KITSU_PLUGIN_DEV_URLS / KITSU_PLUGIN_DEV_PATHS as above).

It is useful to also install a legacy iframe plugin side by side, so you can compare iframe vs injected behaviour. The sample cgwire/kitsu-tickets plugin is a good baseline (Nuxt frontend, no plugin.js):

git clone https://github.com/cgwire/kitsu-tickets.git
zou install-plugin --path ./kitsu-tickets
# restart Zou, then open the tickets plugin in Kitsu — it should stay in an iframe

1. Frontend injection

  1. Build the plugin frontend with defineKitsuPluginConfig() so frontend/dist/plugin.js exists (npm run build in the plugin frontend).
  2. Install: zou install-plugin --path /path/to/plugin (use --force to upgrade), then restart Zou.
  3. Confirm PLUGIN_FOLDER/<id>/frontend/dist/plugin.js is present and served (e.g. /api/plugins/<id>/frontend/plugin.js through Kitsu’s proxy).
  4. Log in and check the user context: the plugin entry has "injected": true.
  5. Open /plugins/<id>/… (studio) or the production plugin route from the topbar. Expect <plugin-host> (not an iframe) and any plugin routes / views mounting inside Kitsu.
  6. Negative check: remove or rename plugin.js, restart Zou, reload context → "injected": false → iframe to /api/plugins/<id>/frontend/.

2. Slot injection (action-topbar-menu)

  1. Declare slots: { 'action-topbar-menu:before': … } and/or 'action-topbar-menu:after' in an injected plugin.
  2. Load the plugin (step 1 above, or HMR with a stub plugin.js).
  3. Select one or more tasks so ActionPanel appears.
  4. Expect your UI before and/or after the host menu items (Status / Assign / …).
  5. Deactivate / HMR unload → the slot content disappears.

3. Provider override (ComboboxStatus.sortedTaskStatusList)

  1. Declare providers: { 'ComboboxStatus.sortedTaskStatusList': fn } with an order that is obviously different from Kitsu’s default.
  2. Load the injected plugin.
  3. Open any ComboboxStatus (ActionPanel “change status”, task UI, …).
  4. Expect the dropdown order to follow your function.
  5. Deactivate / HMR unload → order returns to Kitsu’s sortTaskStatuses.

Constraints

  • Do not name a plugin page after an existing Kitsu route (login, …): the host only rewrites names that are not already registered.
  • Use relative imports inside a plugin (keeps the package portable). An @/ alias is fine on the plugin's own Vite instance, but do not point it at Kitsu's src.
  • Plugin styles are global. Use <style scoped>; the host wraps plugin views in a [data-plugin] element for extra scoping.
  • SCSS is not preprocessed with Kitsu's variables in a plugin build. Use plain CSS with Kitsu's CSS custom properties (--text-primary, --bg-card, ...).