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

@primafuture/contrib-kit-vue

v2.0.0

Published

Vue 3 adapter for Contrib Kit with reactive outlets, scoped registries, managed lazy loading, and SSR support.

Readme

@primafuture/contrib-kit-vue

Vue 3 integration for Contrib Kit Core with scoped registry ownership, reactive resolution, headless outlets, managed-lazy contributions, and SSR-safe rendering.

Installation

pnpm add @primafuture/contrib-kit-core @primafuture/contrib-kit-vue vue

Peer requirements are @primafuture/contrib-kit-core >=1.0.0 <2 and vue >=3.5.0 <4. Import both packages through their public roots; internal and source subpaths are intentionally closed.

Point, registry, and plugin

import * as contribKitCore from "@primafuture/contrib-kit-core";
import * as contribKitVue from "@primafuture/contrib-kit-vue";
import * as vue from "vue";

interface MenuContext {
	query: string;
}

interface MenuItemData {
	readonly label: string;
}

const menuPoint = contribKitVue.defineVueExtensionPoint<
	MenuContext,
	MenuItemData
>()({
	pointId: "example/vue/menu-items",
	contractVersion: 1,
	cardinality: "many",
	filter(contribution, context): boolean {
		return contribution.data.label.toLowerCase().includes(context.query.toLowerCase());
	},
});

const registry = contribKitCore.createExtensionRegistry();
registry.rootScope.registerPoint(menuPoint);

const MenuItem = contribKitVue.defineVueContributionComponent(
	(props: contribKitVue.ExtensionContributionProps<MenuContext, MenuItemData>): vue.VNode =>
		vue.h(
			"span",
			{
				"data-index": props.placement.index,
				"data-count": props.placement.count,
			},
			props.contribution.data.label,
		),
);

const registration = registry.registerContribution(menuPoint, {
	contributionId: "example/vue/menu-items/home",
	expectedContractVersion: 1,
	data: { label: "Home" },
	implementation: MenuItem,
});

await registration.whenActive();

const plugin = contribKitVue.createExtensionPlugin({ registry });
const app = vue.createApp({
	setup() {
		const context = vue.reactive<MenuContext>({ query: "" });
		return (): vue.VNode => vue.h(contribKitVue.ExtensionOutlet, {
			point: menuPoint,
			context,
		});
	},
});

app.use(plugin);
app.mount("#app");

async function disposeHost(): Promise<void> {
	app.unmount();
	plugin.dispose();
	await registry.dispose();
}

One Vue app accepts one root Contrib Kit plugin for its lifetime. The plugin owns only its Vue adapter scope; the host still owns the Core registry.

Managed lazy contributions

const lazyMenuItem = contribKitVue.defineVueLazyContribution<MenuContext, MenuItemData>({
	async load({ signal }) {
		const module = await import("./LazyMenuItem.js");
		if (signal.aborted) {
			throw new DOMException("Aborted", "AbortError");
		}
		return contribKitVue.defineVueContributionComponent(module.default);
	},
});

The server renders the outlet's loading state without starting the loader. On the client, each mounted boundary generation owns one loader attempt and abort signal. Removal, replacement, registry switching, or disposal invalidates the old generation before abort dispatch.

Outlet slots

ExtensionOutlet has three slots scoped to one contribution boundary and two slots scoped to the outlet's resolve result:

| Slot | Scope | Meaning | | --- | --- | --- | | contribution-ready | Each contribution | An eager or loaded managed-lazy component is ready for a render attempt. The slot replaces the default component render. | | contribution-loading | Each contribution | One managed-lazy contribution is waiting for its current client loader generation. | | contribution-error | Each contribution | One contribution generation failed and the effective error policy selected its fallback. | | empty | Whole outlet | Resolution succeeded but returned no contributions. | | resolve-error | Whole outlet | The current resolve attempt failed and can be refreshed. |

All slots are optional. Omitting contribution-ready lets the outlet render the resolved component directly. The per-contribution slots can be invoked independently for several contributions in the same outlet; empty and resolve-error describe one global outlet result.

<contribKitVue.ExtensionOutlet :point="menuPoint" :context="context">
	<template #contribution-loading="state">
		<span>Loading {{ state.contribution.contributionId }}</span>
	</template>
	<template #contribution-error="state">
		<button type="button" @click="state.retry()">Retry {{ state.error.code }}</button>
	</template>
	<template #empty>
		<span>No menu items</span>
	</template>
	<template #resolve-error="state">
		<button type="button" @click="state.refresh()">Resolve again</button>
	</template>
</contribKitVue.ExtensionOutlet>

Logical placement

Every contribution component and every contribution-scoped ready, loading, or error slot receives a required frozen ResolvedContributionPlacement value through placement, with index, count, isFirst, and isLast. Placement describes the contribution's position in the final resolved membership after Core filtering, ordering, and selection. It is deliberately independent of the eventual DOM position: slots and contribution components may render fragments, wrappers, portals, or no element at all.

Retained memberships receive the newest context and placement atomically without remounting their component or restarting an active lazy generation. A renderer can therefore use placement.isFirst or placement.isLast for presentation while preserving component-local state across reordering.

Migrating from Vue adapter 1.x

  • Contribution components must accept the current ExtensionContributionProps, including its required placement member.
  • Contribution-scoped ready, loading, and error slots receive the same required placement snapshot.
  • The successful-empty reason filteredOut was replaced by excludedByPolicy without an alias. It covers any non-empty raw catalog that becomes empty after Core filter, ordering, or selection policy evaluation.
  • empty and resolve-error remain outlet-global and do not receive placement.

Nested registries and reactive context

Call provideExtensionRegistry() from component setup to override the nearest plugin registry for one subtree. An explicit registry on ExtensionOutlet or useResolvedContributions() takes precedence over the nearest provider. Disposing a plugin or provider invalidates only its adapter subtree and never disposes the Core registry.

Resolution tracks only reactive context properties synchronously read by the Core context validator, filter, or ordering policy. Changing an unread property is a no-op; conditional dependencies are collected again on every resolve attempt. Sink, projector, emit, and render callbacks are not resolve dependencies.

For SSR, create a request-local registry, await every required registration's whenActive(), then create the plugin and app. Cleanup remains host-owned: unmount the app, dispose the plugin, and finally dispose the registry.

License

ISC © 2026 PrimaFuture.cz s.r.o.